Reactの再レンダリング連鎖:1クリックでツリー全体が再レンダリングされるとき
何が起きているか
Reactでは、状態更新はそのコンポーネントと、既定では全ての子を再レンダリングします。状態がツリーの上位(コンテキストやページレベルのストア)にあると、1回のキー入力やクリックで数百のコンポーネントが再レンダリングされ得ます — その多くは前とまったく同じものを表示します。
1回のレンダリングは安価ですが、1つのハンドラーで数千回は違います。その作業はINPのprocessing timeフェーズに入ります。
見分け方
- React DevTools Profilerが幅広いフレームグラフを示す:propsが目に見えて変わっていないのにコンポーネントが再レンダリングされる。
- 制御された入力への打鍵がもたつく:1キーごとに大きなリストやテーブルが再レンダリングされる。
- INPでprocessing timeが支配的で、DevTools PerformanceのスタックがReact内部でいっぱい。
直し方
- 状態を下げる:ページレベルのコンポーネントやコンテキストではなく、それを必要とする最小のコンポーネントに置く。
- 重い部分木をReact.memoで包み、渡すpropsをuseCallback/useMemoで安定させる — レンダリングのたびに新しいコールバックを作るとmemoは無意味。
- 緊急でない更新(入力中のリストのフィルタ)をuseTransitionでマークし、緊急のレンダリング(入力そのもの)を先に描画する。
問題
const Page = () => {
const [query, setQuery] = useState("")
// every keystroke re-renders Page → Header, Sidebar, BigTable, Footer
return <>
<SearchInput value={query} onChange={setQuery} />
<BigTable rows={filterRows(rows, query)} />
</>
}
修正
const BigTable = React.memo(({ rows }) => /* ... */)
const Page = () => {
const [query, setQuery] = useState("")
const [isPending, startTransition] = useTransition()
const rows = useMemo(() => filterRows(allRows, query), [query])
const onChange = (q) =>
startTransition(() => setQuery(q)) // input paints first, table follows
return <>
<SearchInput onChange={onChange} />
<BigTable rows={rows} />
</>
}
サイトを無料でスキャン →