お疲れ様です。最近、プロダクトのパフォーマンス改善チケットを次々と片付けていることと思います。
さて、コードレビューをしていると、やたらとあちこちに `React.memo` を貼り付けているコードを見かけませんか?「とりあえず再レンダリングを防ぎたいから `memo` で囲む」というアプローチ、実はこれ、フロントエンド開発において最もやりがちなアンチパターンのひとつです。
今回は、`React.memo` の正体と、その本領を発揮するための「Propsの比較最適化」、そして現場で絶対に知っておくべきカスタム比較関数の型定義について、骨の髄まで解説していきましょう。
—
1. `React.memo` の本質と、ブラウザの裏側で起きていること
まず大前提として、Reactにおける「再レンダリング」とは何でしょうか? それは、「コンポーネントの関数がもう一度実行され、新しい仮想DOM(JSXの戻り値)が生成されるプロセス」に過ぎません。
親が再レンダリングされると、その子孫コンポーネントもデフォルトでは上から順に容赦なく関数が再実行されます。ここで `React.memo` を使うと、Reactはこう囁きます。
> 「おい、前回のPropsと今回のPropsが同じなら、わざわざこの重いコンポーネント関数を実行して仮想DOMを作り直すのはやめておこうぜ」
参照の壁:JavaScriptがオブジェクトを比較するとき
しかし、ここに大きな罠があります。JavaScriptの基本ですが、オブジェクトや配列は「参照(メモリ上の住所)」で比較されます。
const obj1 = { id: 1 };
const obj2 = { id: 1 };
console.log(obj1 === obj2); // false (中身が同じでも別物!)
親コンポーネントが再レンダリングされるたびに、新しいオブジェクトリテラル(`{}`)やインライン関数(`() => {}`)が作られると、`React.memo` は「おっと、Propsの住所が変わったぞ!再レンダリングしなきゃ!」と判断します。つまり、安易な `React.memo` は、比較のためのコスト(浅い比較の計算量)を毎回のレンダリングに追加するだけの「お荷物」になり下がるのです。
—
2. 実務で使える:カスタム比較関数(`arePropsEqual`)の極意
デフォルトの `React.memo` は、Propsの「浅い比較(Shallow Equal)」を行います。しかし、実務では「ネストしたオブジェクトの一部が変わっていないなら再レンダリングをスキップしたい」「特定のIDだけ見ておけばいい」という複雑な要件に出くわします。
ここで登場するのが、`React.memo` の第2引数であるカスタム比較関数です。
現場でコピペして使える実例コード
ダッシュボードやデータグリッドなど、大量の行を描画するシチュエーションを想像してください。ユーザー名と最終ログイン日時を持つユーザーカードコンポーネントを最適化してみましょう。
import React, { memo } from ‘react’;
// 1. Propsの型定義
type UserCardProps = {
user: {
id: string;
name: string;
lastLogin: string;
// その他、頻繁に更新されるメタデータなどがあると仮定
metadata: {
loginCount: number;
sessionToken: string;
};
};
onSelect: (id: string) => void;
};
// 2. カスタム比較関数の実装
// 返り値が true なら「再レンダリングをスキップ(変更なし)」、false なら「再レンダリングを実行」
const areUserCardPropsEqual = (
prevProps: UserCardProps,
nextProps: UserCardProps
): boolean => {
// ユーザーIDと名前、そして実質的な表示に関わるデータだけを比較する
// metadata の中の sessionToken が変わっても、画面表示に関係なければ再レンダリングさせない、といった制御が可能
return (
prevProps.user.id === nextProps.user.id &&
prevProps.user.name === nextProps.user.name &&
prevProps.user.lastLogin === nextProps.user.lastLogin &&
// 関数は原則として親側で useCallback 等でメモ化されている前提だが、
// 万が一の参照ズレを無視したい場合のガードとしても比較関数は機能する
prevProps.onSelect === nextProps.onSelect
);
};
// 3. React.memo にカスタム比較関数を渡してラップする
export const UserCard: React.FC
console.log(`[UserCard Render]: ${user.name}`);
return (
style={{ padding: ’16px’, border: ‘1px solid #ccc’, margin: ‘8px 0’, cursor: ‘pointer’ }}
>
{user.name}
最終ログイン: {user.lastLogin}
);
}, areUserCardPropsEqual);
UserCard.displayName = ‘UserCard’;
このコードのポイント
- 関数の型定義のスマートさ: 特別なユーティリティ型を持ち出さなくても、コンポーネントのProps型(ここでは `UserCardProps`)をそのまま比較関数の引数に適用できます。
- 無駄な再レンダリングの根絶: 画面描画に関係のない `metadata` の一部が書き換わっても、このカスタム比較関数が `true` を返すため、DOMのツリー構築コストを劇的に削減できます。
- デバッグのしやすさ: コンポーネントに `displayName` を明示しておき、レンダリングログを仕込むことで、「本当にメモ化が機能しているか」をブラウザのコンソールで即座に検証できます。
—
3. シニアから後輩への実践的なアドバイス(ベストプラクティス)
`React.memo` を導入する際は、以下の鉄則をチームメンバーと共有してください。
1. 「まずは計測せよ(Premature Optimizationの回避)」
Reactのレンダリングは、私たちが想像するよりも遥かに高速です。本当にパフォーマンスのボトルネックになっている箇所(React DevToolsのProfilerで測定済み)以外には、最初から `memo` を貼らないこと。コードベースが複雑化し、かえってバグの温床になります。
2. 親側の `useCallback` / `useMemo` とセットで考える
子コンポーネントを `React.memo` で包んでも、親コンポーネントが毎回新しい関数インスタンスをPropsとして渡していたら、意味がありません。最適化は「親から子への矢印(データフロー)」の両端を抑える必要があります。
3. カスタム比較関数を書きすぎない
カスタム比較関数の中身が複雑すぎると、「Propsを比較するための計算コスト」が「コンポーネントを再レンダリングするコスト」を上回るという本末転倒な現象が起きます。比較はシンプルに、O(1) で終わるプリミティブ値の比較に留めるのがプロの技です。
—
まとめ
`React.memo` と Props の比較最適化は、正しく使えば巨大なアプリケーションを救う強力な武器になりますが、闇雲に使えばコードのメンテナンス性を下げる諸刃の剣です。
「なぜここで再レンダリングが起きているのか」「本当にこのオブジェクトの参照は変わるべきではないのか」を、ブラウザの裏側の動き(JavaScriptのメモリ管理や仮想DOMのライフサイクル)まで想像しながらコードを書けるようになると、フロントエンドエンジニアとしての視野が一段と広がります。
明日からのコードレビューや実装に、ぜひこの知見を活かしてください。一緒に最高のプロダクトを作り上げていきましょう!

コメント