目次
概要
前回のブログ記事では、Reactのレンダリングがいつ発生し、どのような仕組みで行われるかについて詳しく見てきました。Reactのレンダリングについて詳しく知りたい方は、以下のリンクを参照してください。
今回の記事では、レンダリングの最適化について掘り下げていきます。
レンダリングパフォーマンス最適化の必要性
レンダリングはReactの正常な動作です。問題なのは、**「不要な」**レンダリングです。この不要なレンダリングこそが、最適化の対象になります。
不要なレンダリングとは、出力が変わらないレンダリングのことです。つまり、生成されたFiberオブジェクトが前回のレンダリングと同じ内容だった場合を指します。
ソフトウェアにおける最適化のアプローチは、大きく分けて2つあります。
- 同じ処理をより速く実行する(高速化)
- 同じ処理をスキップする
Reactでは、不要なレンダリングをスキップすることでレンダリング処理を減らし、パフォーマンスを最適化しています。
- Reactのレンダリング最適化における基本原則
コンポーネントのレンダリング出力は、現在のpropsとstateに完全に基づいているべきであり、props/stateが変わっていなければ出力も同じであるべきです。— Reactコンポーネントの純粋性
この原則に基づくと、propsとstateが変わっていないことが分かれば、レンダリング結果は前回と同じだと予測できます。つまり、レンダリングをスキップしても問題ない — これがReactのレンダリング最適化の前提です。
レンダリングの最適化方法
Reactには、レンダリングをスキップするための主要なAPIが3つ用意されています(関数コンポーネント用1つ、クラスコンポーネント用2つ)。
関数コンポーネント
1. React.memo()
propsが変更されたかどうかをチェックするHOC(Higher-Order Component)です。
const MemoizedComponent = React.memo(MyComponent);
propsが変わっていなければ、レンダリングをスキップします。
クラスコンポーネント
2. shouldComponentUpdate
shouldComponentUpdate(nextProps, nextState) {
return !shallowEqual(this.props, nextProps) ||
!shallowEqual(this.state, nextState);
}
このメソッドがfalseを返すと、レンダリングがスキップされます。
3. PureComponent
shouldComponentUpdateを自動的に実装してくれる基底クラスです。
class MyComponent extends React.PureComponent {
// ...
}
浅い等価性(Shallow Equality)
これらのメソッドはすべて**「浅い等価性」**を使って値を比較します。そのため、以下のような問題が発生する可能性があります。
- オブジェクト内のフィールドが変わっても、参照が同じなら同一とみなされる(参照が変わらない問題)
- 値が同じでも新しいオブジェクトなら、参照が異なるため別物とみなされる(参照が変わる問題)
最適化における参照の問題
Reactではデフォルトで、親がレンダリングされると子も必ずレンダリングされるため、新しいprop参照は特に問題になりません。
function ParentComponent() {
const handleClick = () => {
console.log('Button clicked');
};
const data = { a: 1, b: 2 };
return <NormalChildComponent onClick={handleClick} data={data} />;
}
- レンダリングのたびに新しい関数とオブジェクトの参照が生成されます
NormalChildComponentはどのみちレンダリングされるので問題ありません
しかし、memoを使って子コンポーネントのレンダリングを最適化した場合、親のレンダリングで新しい参照が生まれてしまい、子コンポーネントの不要な再レンダリングが発生します。
const MemoizedChildComponent = React.memo(ChildComponent);
function ParentComponent() {
const handleClick = () => {
console.log('Button clicked');
};
const data = { a: 1, b: 2 };
return <MemoizedChildComponent onClick={handleClick} data={data} />;
}
問題点:
- レンダリングのたびに新しい
onClickとdataの参照が生成される - 新しい参照がpropsの変更として検出される
MemoizedChildComponentは毎回再レンダリングされてしまうReact.memo()が意味をなさなくなる!
Propsの参照を最適化する
関数コンポーネントには、この参照問題を解決するために2つのフックが用意されています。
useCallback:関数のメモ化
function ParentComponent() {
const memoizedCallback = useCallback(() => {
console.log('Clicked');
}, []); // 依存配列が空なので絶対に変わらない
return <MemoizedChild onClick={memoizedCallback} />;
}
useMemo:データのメモ化
function ParentComponent() {
const memoizedData = useMemo(() => ({ a: 1, b: 2 }), []);
return <MemoizedChild data={memoizedData} />;
}
useCallbackとuseMemoを使うことで、子コンポーネントに渡すpropsの参照を安定させ、意図したレンダリング最適化が正しく機能するようになります。
function ParentComponent() {
const memoizedCallback = useCallback(() => {
console.log('Clicked');
}, []);
const memoizedData = useMemo(() => ({ a: 1, b: 2 }), []);
return <MemoizedChild onClick={memoizedCallback} data={memoizedData} />;
}
すべてをメモ化すべきか?
Reactのレンダリング最適化の話題では必ず出てくる質問です。答えは**「いいえ」**です。
なぜなら、メモ化自体にもオーバーヘッドがあるからです。
- propsの比較に時間がかかる
- メモリを消費する
- コードが複雑になる
メモ化が有効なケース
React公式ドキュメントでは、メモ化がいつ有効かについて次のように説明しています。
「memoによる最適化は、コンポーネントが同じpropsで頻繁に再レンダリングされ、再レンダリングのロジックのコストが高い場合にのみ有効です。」
レンダリング最適化のガイドライン
メモ化のオーバーヘッドと公式ドキュメントのガイダンスを踏まえると、次のようなレンダリング最適化のガイドラインが導けます。
- コンポーネントが同じpropsで頻繁に再レンダリングされている場合
- 再レンダリングのロジックのコストが高い場合(複雑な計算、多数の子コンポーネント)
- 実際にパフォーマンスの問題を計測で確認した場合
Reactアプリで発生するパフォーマンスの問題のほとんどは、再レンダリングとは無関係な可能性が高いです。レンダリングを最適化する前に、まずロジックに問題がないか、不要なstate更新をしていないかを確認するのがおすすめです。
Reactにおける不変性(Immutability)
Reactはデフォルトで浅い等価性チェックを使うため、データの中身が変わっても参照が変わらなければ再レンダリングが実行されないという問題があります。
そのため、参照ではなく値が変更されても浅い等価性チェックで検出できないケースが発生します。
// ❌ ダメな例:変更が検知されない
const [todos, setTodos] = useState(someTodosArray);
const handleClick = () => {
todos[3].completed = true;
setTodos(todos); // 同じ参照なのでReactが変更を検知できない
};
この問題を避けるには、新しい参照を生成してから値を変更し、Reactに参照が変わったことを明確に伝える必要があります。
// ✅ 良い例:新しい参照を生成
const handleClick = () => {
const newTodos = todos.slice();
newTodos[3].completed = true;
setTodos(newTodos); // 新しい参照なので変更が検知される
};
Reactとそのエコシステムは、不変な更新を前提として設計されています。 不変性を守らないと、バグのリスクが大きくなります。
Reactコンポーネントのレンダリングパフォーマンス計測
先ほどのレンダリング最適化のガイドラインで、実際にパフォーマンスの問題を計測した場合にのみレンダリングの最適化を行うべきだと述べました。Reactには、レンダリング回数を計測できるReact DevTools Profilerが用意されています。
- https://ja.react.dev/learn/react-developer-tools
- https://chromewebstore.google.com/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en&pli=1
Reactでレンダリングの最適化を行う前に、必ずこのツールで現在のレンダリング状態を分析し、最適化後の結果と比較して、本当に意味のある改善になっているか確認しましょう。
<StrictMode>
Reactは開発環境ではデフォルトで<StrictMode>が有効になっており、その中のコンポーネントを二重レンダリングします。
二重レンダリングの目的は、レンダリング中の副作用をReactが検知するためです。
そのため、console.logでレンダリング回数を確認した場合、実際のレンダリング回数とは異なることがあります。レンダリング最適化のために正確なレンダリング回数を把握するには、必ずReact DevTools Profilerを使いましょう。
React 19のReact Compiler
React 19でReact Compilerが導入されました。React Compilerは、Reactアプリを自動的に最適化する新しいビルドタイムツールです。
- React Compiler:https://ja.react.dev/learn/react-compiler
React Compilerを使わない場合、以下のようにメモ化を駆使してレンダリングを最適化することになります。
import { useMemo, useCallback, memo } from 'react';
const ExpensiveComponent = memo(function ExpensiveComponent({ data, onClick }) {
const processedData = useMemo(() => {
return expensiveProcessing(data);
}, [data]);
const handleClick = useCallback((item) => {
onClick(item.id);
}, [onClick]);
return (
<div>
{processedData.map(item => (
<Item key={item.id} onClick={() => handleClick(item)} />
))}
</div>
);
});
しかし、このメモ化は実は正しく最適化できていません。以下の部分で、レンダリングのたびに新しい参照が生まれてしまうからです。
<Item key={item.id} onClick={() => handleClick(item)} />
このように、手動でのレンダリング最適化はとても気を使う作業で、ほんの少しの油断で小さなミスが入り込み、最適化が台無しになることがよくありました。
この問題を解決するために、ReactチームはReact Compilerを開発しました。React Compilerを使えば、何も特別なことをしなくても、自動的にレンダリングが最適化されます。
function ExpensiveComponent({ data, onClick }) {
const processedData = expensiveProcessing(data);
const handleClick = (item) => {
onClick(item.id);
};
return (
<div>
{processedData.map(item => (
<Item key={item.id} onClick={() => handleClick(item)} />
))}
</div>
);
}
React CompilerはReact 17、18、19をサポートしています。
レンダリングの最適化で悩んでいるなら、React Compilerの導入を検討してみてはいかがでしょうか。
おわりに
今回の記事では、Reactのレンダリング最適化について見てきました。また、React Compilerという新しいレンダリング最適化の未来についても触れました。
ぜひReact Compilerを導入して、楽にレンダリングを最適化してみてください。もしReact Compilerの導入が難しい場合は、この記事を通じてReactのレンダリング最適化をしっかり理解した上で、実践に活かしていただければ幸いです。
私のブログが役に立ちましたか?下にコメントを残してください。それは私にとって大きな大きな力になります!
アプリ広報
Dekuが開発したアプリを使ってみてください。Dekuが開発したアプリはFlutterで開発されています。興味がある方はアプリをダウンロードしてアプリを使ってくれると本当に助かります。