「なんか重い」を構造から見抜く

Reactをレビューしていて「ちょっとこの画面、重くない?」と思ったことありませんか?
実装者が気づかないところで、再レンダリングが無駄に起きている構造がけっこう埋まっています。

この記事では、レビューアーが見落としがちな“再レンダリングを誘発する構造”に注目して、どう気づき、どうリファクタを提案していくかを整理します。

よくある再レンダリングの原因

まず、Reactで不要な再レンダリングが起きやすい代表パターンをざっと挙げます。

  • propsにインライン関数やオブジェクトリテラルを渡している
  • 状態が親に集まりすぎていて、子も全部巻き込まれている
  • Context経由で巨大な値を毎回配ってしまっている
  • リスト構造の中で描画コストが高いアイテムを多量に持っている
  • useEffect依存の変化を拾ってsetStateを何度も発火している

これらは「パッと見」では気づきにくいですが、ちゃんとパターン化されているので、レビューで構造を見るときに意識しておくと検知しやすくなります。

インライン関数が地味に効いてくる

フォーム入力のコード
export function ProfileForm() {
  const [name, setName] = useState('');
  return (
    <Input
      label="名前"
      value={name}
      onChange={e => setName(e.target.value)}
@Reviewer
毎回新しい関数参照が生成されているため、Inputが再レンダリングされます。
useCallbackで安定化させましょう
/> ); }

入力のたびに Input が描画され直します。たとえ Input 側で React.memo を使っていても、関数の参照が変わっていると無意味なんですね。

改善例
const handleChange = useCallback(
  (e: React.ChangeEvent<HTMLInputElement>) => setName(e.target.value),
  []
);

<Input label="名前" value={name} onChange={handleChange} />

このワンステップだけで再レンダリングは止まります。
レビューでは、propsに直接無名関数を入れていないかチェックポイントとして持っておくと便利です。

Contextが巨大になっているケース

contextの再生成例
export const ThemeContext = createContext({ color: '#000', toggle: () => {} });

export function ThemeProvider() {
  const [dark, setDark] = useState(false);
  const value = {
    color: dark ? '#fff' : '#000',
    toggle: () => setDark(v => !v),
@Reviewer
Contextのvalueが毎回新しいオブジェクトになるため、
Consumer全体が再レンダリングされます。
useMemoでvalueを安定させましょう
}; return <ThemeContext.Provider value={value}>{/* children */}</ThemeContext.Provider>; }

valueのオブジェクトが毎回生成されているため、下層のすべてのConsumerが更新のたびに再描画されます。

useMemoの活用
const value = useMemo(
  () => ({ color: dark ? '#fff' : '#000', toggle: () => setDark(v => !v) }),
  [dark]
);

とても小さな改善ですが、ページ全体のレンダリング負荷に大きく影響することがあります。

揺れの波及を見てみる

UML Diagram

ThemeProviderのvalueが変わるたびに、HeaderもSidebarもContentも再描画される。こういう連鎖が“なんか重い”の正体です。

表でざっくり構造を把握する

よくある構造 見抜き方 改善の方向性
propsにインライン関数 毎回render時に生成されていないか? useCallback
Contextがでかい Providerのvalueが毎回新規オブジェクト? useMemo / 分割提供
リストが重い リスト項目がuseEffectなど持っていないか? React.memo / react-window
親が全部のstate持ってる 子が毎回巻き込まれていないか? useReducerで分離 or コンテナ導入

コメント例:よくある改善提案

コメント例
- コンポーネントが重いです
+ onChangeに渡している無名関数が毎回新しくなっており、
+ InputRowがレンダリングされ直しています。
+ useCallbackで関数参照を固定することで、再描画を抑制できます。
コメントテンプレート
@Reviewer: この箇所は実行時には問題に見えませんが、Contextのvalueが毎回新しくなっていることでツリー全体の再描画が発生しています。
@Reviewer: DevToolsのFlamegraphで確認したところ、HeaderとSidebarが何度も再描画されています。

まとめ

再レンダリングの問題は細かいところでジワジワ積もるタイプの構造です。
パフォーマンスを改善するために難しい最適化を行うより、まずレビューで「構造的に揺れやすくなってないか」を抑えるだけで、大きな改善につながることが多いです。

レビューアーとしては、propsの参照安定性・Contextの提供方式・リスト構造の描画コストあたりを重点的に見ていくのがおすすめです。
「気になるけど測定しにくい」場合は、ChromeのPerformanceタブでサクッとFlamegraphを取るだけでも十分な根拠になります。