こんにちは。現場の最前線で毎日コードと向き合っていると、「動くには動くけれど、どうもパフォーマンスが怪しい」「親の再描画に引きずられて、子コンポーネントが無駄にレンダリングされている気がする」という壁にぶつかることがよくありますよね。
中級からもう一段階上のシニアへステップアップしようとしている君なら、Reactのレンダリングメカニズムの基本(親がレンダリングされれば、子もデフォルトで再描画される仕様)はもう頭に入っているはずです。
そこで今回は、現場で最もよく遭遇し、かつ誤った対応をしてしまいがちな「Propsとして渡すオブジェクトや配列のメモ化(`useMemo`)」について、ブラウザの裏側の動きも含めて徹底的に解説します。適当にメモ化を乱用して逆にパフォーマンスを落とす「アンチパターン」から抜け出し、真に意味のある最適化を手に入れましょう。
—
なぜオブジェクトや配列のPropsは厄介なのか?
まずは、Reactが裏側でどうやって値の変化を検知しているのか、その根本を思い出してください。
JavaScriptにおいて、プリミティブ型(文字列や数値など)は「値」で比較されますが、オブジェクトや配列は「参照(メモリ上のアドレス)」で比較されます。つまり、コンポーネントが再描画されるたびに、関数内でリテラルとして定義されたオブジェクトや配列は、中身が全く同じであっても、メモリ上では「全く別物の新しい箱」として生成されるのです。
// 親が再描画されるたびに、毎回「新しいアドレス」を持つオブジェクトが生まれる
const user = { name: “Taro”, role: “admin” };
子コンポーネントが `React.memo` でラップされていたとしても、親から渡されるこの `user` プロパティの「参照」が変わっているため、Reactは「おっと、Propsが変わったぞ!再描画しなきゃ!」と判断してしまいます。これが、無駄な再描画を引き起こす元凶です。
ブラウザの裏側とReactのレンダリングサイクル
ここでブラウザの挙動に目を向けてみましょう。JavaScriptのエンジンは、不要になったオブジェクトを「ガベージコレクション(GC)」によって回収します。
親コンポーネントが再描画されるたびに、数多くのオブジェクトや配列のリテラルがヒープ領域にばらまかれ、古いものはGCの回収対象になります。これ自体は現代のJSエンジンなら高速に処理しますが、問題はそれに伴う「Reactの仮想DOMの差分比較(Reconciliation)」と「実際のDOMツリーへの反映コスト」です。
子コンポーネントが巨大なツリーを持っていたり、複雑なCSS計算やDOM操作を伴う場合、無駄な再描画は一瞬の「カクつき(フレームレートの低下)」としてユーザー体験を直撃します。ここで `useMemo` の出番です。依存配列(dependencies)が変わらない限り、同じメモリー上の参照を維持し続けることで、Reactに「子を再描画しなくていいよ」とシグナルを送るわけです。
—
現場で使える!実践コードパターン
口で言うだけでは説得力がないので、実務でそのまま使えるクリーンなコードを見ていきましょう。
今回は、検索フィルターの条件(オブジェクト)と、表示するリストのデータ(配列)を子コンポーネントに渡すシチュエーションを想定します。
import React, { useState, useMemo } from ‘react’;
// ==========================================
// 子コンポーネント (React.memoで不要な再描画をブロック)
// ==========================================
type SearchFilter = {
keyword: string;
category: string;
};
interface UserListProps {
filter: SearchFilter;
tags: string[];
onSelect: (id: number) => void;
}
const UserList: React.FC
console.log(‘🔥 UserList(子)が再描画されました!’);
return (
ユーザー検索結果
検索キーワード: {filter.keyword || ‘未入力’}
カテゴリー: {filter.category}
タグ数: {tags.length}
{/ リスト描画のモック /}
);
});
// 表示名を設定しておく(DevToolsでのデバッグをしやすくするプロの技)
UserList.displayName = ‘UserList’;
// ==========================================
// 親コンポーネント
// ==========================================
export const ParentComponent: React.FC = () => {
const [keyword, setKeyword] = useState
const [category, setCategory] = useState
const [count, setCount] = useState
// 1. オブジェクトのPropsをuseMemoでメモ化
// keywordやcategoryが変わらない限り、同じ参照を保持し続けます
const memoizedFilter = useMemo
return {
keyword,
category,
};
}, [keyword, category]);
// 2. 配列のPropsをuseMemoでメモ化
// 固定の配列であっても、コンポーネント内でリテラル定義している場合はメモ化対象になります
const memoizedTags = useMemo
return [‘React’, ‘TypeScript’, ‘Frontend’];
}, []); // 依存配列が空なので、マウント時の参照を永続的に保持
// 3. コールバック関数もメモ化(今回は本筋ではないですが、セットで使うのが定石)
const handleSelect = useMemo(() => {
return (id: number) => {
console.log(`Selected user ID: ${id}`);
};
}, []);
return (
パフォーマンス最適化ダッシュボード
{/ 関係のない親のステートを変更するボタン /}
/>
{/ 子コンポーネントへメモ化したPropsを渡す /}
);
};
コードの解説とシニアからのワンポイントアドバイス
1. `React.memo` とのセット運用が絶対条件
`useMemo` でオブジェクトや配列の参照を固定しても、受け取る側の子コンポーネントが `React.memo` で包まれていなければ、親の再描画に伴って結局子も再描画されます。メモ化は「ペア」で初めて効果を発揮することを忘れないでください。
2. 依存配列(dependencies array)の管理に妥協しない
`useMemo` の第二引数には、その計算結果(今回の場合はオブジェクトや配列の生成)に影響を与える変数を全て正確に記述します。ESLintの `react-hooks/exhaustive-deps` ルールを厳格に守りましょう。ここをごまかすと、古い状態を参照し続ける厄介なバグ(Stale Closure)の温床になります。
—
最後に:過剰最適化(Premature Optimization)の罠
ここまで `useMemo` の重要性を語ってきましたが、最後にチーフアーキテクトとして一番大切な注意点を伝えておきます。
「なんでもかんでも `useMemo` で包めばアプリが速くなる」というのは大いなる勘違いです。
`useMemo` 自体も、コンポーネントがレンダリングされるたびに依存配列の比較処理を行っています。極めて軽量なオブジェクトや配列、あるいは子コンポーネントの描画コストが微小な場合、`useMemo` を使うコスト(メモリ消費と比較処理のオーバーヘッド)の方が高くなる「過剰最適化」に陥ります。
実務での判断基準としては、以下のようなケースに絞って適用するのがベストプラクティスです。
- `React.memo` で最適化したい重い子コンポーネントへ、オブジェクトや配列を渡す場合
- そのオブジェクト/配列を、さらに別のフック(`useEffect` など)の依存配列に指定している場合
コードを書くときは常に、「本当にこのメモ化はレンダリングコストの削減に貢献しているか?」というエンジニアリングの視点を持つようにしてください。
チームの開発効率とパフォーマンスを両立させる洗練されたコードを、明日からの実装でもガンガン取り入れていきましょう!

コメント