Reactを用いたWebアプリケーション開発において、コンポーネントの不要な再レンダリングはパフォーマンス低下の要因となります。本記事では、実務で使える再レンダリングの調査方法と、不要な更新を削減するための具体的な実装パターン、およびチューニングの判断基準を解説します。
読者が抱える課題
- アプリケーションの動作が重いが、どのコンポーネントが原因で再レンダリングされているか特定できない
-
React.memoやuseCallbackを導入したものの、意図した通りに再レンダリングが抑制されていない - どのような基準でメモ化を適用すべきか、チーム内での共通指針がない
この記事で分かること
- React Developer Tools を用いた再レンダリング原因の調査手順
- 不要な再レンダリングを削減する3つの実装パターン(良い例・悪い例)
- メモ化を適用する際の判断基準チェックリスト
前提条件
- React 18以降
- TypeScriptを使用した関数コンポーネントの開発環境
1. 再レンダリング原因の調査手順
不要な再レンダリングを削減する前に、まずは「どこが」「なぜ」再レンダリングされているかを特定する必要があります。ブラウザの拡張機能である React Developer Tools を使用した調査手順を解説します。
手順1: Profilerによる計測
- 開発者ツールの「Profiler」タブを開きます。
- 歯車アイコン(Profiler Settings)をクリックし、"Record why each component rendered while profiling." にチェックを入れます。これにより、再レンダリングの理由(どのPropsやStateが変化したか)が記録されます。
- 録画ボタン(青い丸)を押し、調査したい操作(ボタンクリックや入力など)を画面上で行います。
- 録画を停止します。
手順2: ボトルネックの特定
- Flamegraph チャート: 各コンポーネントのレンダリング所要時間が色別(黄色が重く、青が軽い)で表示されます。幅が広いコンポーネントほど処理時間がかかっています。
- Ranked チャート: レンダリング時間が長かった順にコンポーネントが並びます。パフォーマンスに影響を与えている上位のコンポーネントを特定するのに適しています。
手順3: 原因の確認
特定のコンポーネントをクリックすると、右側の詳細パネルに "Why did this render?" という項目が表示されます。
Props changed: [key]State changed-
Hook changed
などの情報から、不要な更新を引き起こしているトリガーを特定します。
2. 不要な更新を減らす実装パターン
原因を特定した後、コードレベルで最適化を行います。よくある原因と対策の実装例を紹介します。
パターンA: オブジェクト・関数参照の固定化
親コンポーネントがレンダリングされるたびに、インラインで定義されたオブジェクトや関数は新しい参照(異なるメモリ番地)として生成されます。これにより、子コンポーネントが React.memo 化されていても再レンダリングが発生します。
悪い例(毎回新しい参照が作られる)
import React, { useState } from 'react';
const ChildComponent = React.memo(({ onClick, options }: { onClick: () => void; options: { theme: string } }) => {
console.log('Child rendered');
return <button onClick={onClick}>{options.theme}</button>;
});
ChildComponent.displayName = 'ChildComponent';
export const ParentComponent = () => {
const [count, setCount] = useState(0);
// 親のStateが更新されるたびに、新しい関数とオブジェクトが生成される
const handleClick = () => console.log('clicked');
const config = { theme: 'dark' };
return (
<>
<button onClick={() => setCount(count + 1)}>Increment ({count})</button>
<ChildComponent onClick={handleClick} options={config} />
</>
);
};
良い例(useCallback と useMemo による参照の固定)
import React, { useState, useCallback, useMemo } from 'react';
const ChildComponent = React.memo(({ onClick, options }: { onClick: () => void; options: { theme: string } }) => {
console.log('Child rendered');
return <button onClick={onClick}>{options.theme}</button>;
});
ChildComponent.displayName = 'ChildComponent';
export const ParentComponent = () => {
const [count, setCount] = useState(0);
// 依存配列が空のため、初回レンダリング時の参照を維持する
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
const config = useMemo(() => ({
theme: 'dark'
}), []);
return (
<>
<button onClick={() => setCount(count + 1)}>Increment ({count})</button>
<ChildComponent onClick={handleClick} options={config} />
</>
);
};
パターンB: コンポーネント構成の工夫(Childrenの活用)
メモ化(useMemo や useCallback)を多用するとコードの可読性が下がり、依存配列の管理コストが増加します。コンポーネントの構造を変更することで、メモ化を使わずに再レンダリングを防ぐ手法が有効です。
悪い例(重いコンポーネントが巻き添えで再レンダリングされる)
import React, { useState } from 'react';
const HeavyComponent = () => {
// レンダリングコストが高いコンポーネント
console.log('HeavyComponent rendered');
return <div>Heavy Content</div>;
};
export const ScrollContainer = () => {
const [scrollPosition, setScrollPosition] = useState(0);
return (
<div onScroll={(e) => setScrollPosition(e.currentTarget.scrollTop)}>
<p>Scroll: {scrollPosition}</p>
{/* スクロールするたびにHeavyComponentも再レンダリングされる */}
<HeavyComponent />
</div>
);
};
良い例(children として受け取ることで再レンダリングを回避)
import React, { useState } from 'react';
const HeavyComponent = () => {
console.log('HeavyComponent rendered');
return <div>Heavy Content</div>;
};
// スクロール状態を管理するラッパーコンポーネント
const ScrollProvider = ({ children }: { children: React.ReactNode }) => {
const [scrollPosition, setScrollPosition] = useState(0);
return (
<div onScroll={(e) => setScrollPosition(e.currentTarget.scrollTop)}>
<p>Scroll: {scrollPosition}</p>
{children}
</div>
);
};
export const App = () => {
return (
<ScrollProvider>
{/* HeavyComponentはAppのコンテキストで評価されるため、ScrollProviderのState更新の影響を受けない */}
<HeavyComponent />
</ScrollProvider>
);
};
3. 実務用:メモ化適用の判断基準チェックリスト
すべてのコンポーネントや関数を無条件にメモ化することは、メモリ消費や比較処理のオーバーヘッドを増やすため推奨されません。以下のチェックリストを基準に、適用の要否を判断してください。
| 項目 | チェック内容 | 適用推奨度 | 理由・対策 |
|---|---|---|---|
| 1 | 子コンポーネントの描画コストが非常に高い(グラフ描画、大量のリストなど) | 高 (適用すべき) |
React.memo を適用し、Propsの変更時のみ再描画させる。 |
| 2 | 子コンポーネントにオブジェクトや関数をPropsとして渡している | 中 (状況による) | 子コンポーネントが React.memo 化されている場合のみ、親側で useMemo / useCallback を使用する。 |
| 3 | プリミティブ値(string, number, boolean)のみをPropsに持つ | 低 (不要な場合が多い) | Reactのデフォルトの比較処理が高速なため、描画コストが低ければメモ化のメリットが少ない。 |
| 4 | 頻繁に値が変化するStateに直接依存している | 低 (効果がない) | 毎フレーム値が変わるような処理では、メモ化の比較処理自体がオーバーヘッドになる。 |
4. 導入時の注意点とよくある失敗
依存配列(Dependency Array)の指定漏れ
useCallback や useMemo を使用する際、内部で参照しているStateやPropsを依存配列に指定し忘れると、古い値(クロージャによる古い状態)を参照し続けるバグ(Stale Closure)の原因になります。
対策: eslint-plugin-react-hooks の exhaustive-deps ルールを有効化し、静的解析で検知できるように設定してください。
React.memo の単体適用
親コンポーネントから渡す関数やオブジェクトがメモ化されていない状態で、子コンポーネントに React.memo だけを適用しても、参照が変わるため再レンダリングを防げません。必ず「渡す側の参照固定」と「受け取る側のメモ化」をセットで設計してください。
まとめ
Reactのパフォーマンスチューニングは、推測ではなく React Developer Tools による計測 から始めることが鉄則です。不要な再レンダリングを発見した際は、安易にすべてをメモ化するのではなく、コンポーネントの分割や children を使った構造の最適化を検討し、それでも解決しない場合に React.memo や useCallback を適切に組み合わせて適用してください。
※Reactの将来のバージョン(React Compilerなど)では、これらの手動最適化が自動化される可能性があります。実装の際は、利用しているReactバージョンの最新の公式ドキュメントを確認してください。