🧘

【パフォーマンス最適化】React開発者 Dan Abramov氏が教えるmemo()を使う前に確認すべきこと

に公開
2

Reactのパフォーマンス最適化と聞くと、すぐに memouseMemo を思い浮かべる方が多いかもしれません。しかし、それらを使う前に試してほしい、より基本的でかつ強力なテクニックあります。

この記事では、React開発者のDan Abramov氏のブログ記事「Before You memo()」をベースに、コンポーネントの構造を変えるだけで不要な再レンダリングを防ぐ方法を紹介します。

パフォーマンス最適化の基本ステップ

特定のステート更新が遅いと感じた場合、memoを導入する前に確認すべき手順があります。

  1. プロダクションビルドか確認する: 開発ビルドは意図的に低速になる
  2. ステートの位置を確認する: ステートを必要以上にツリーの高い位置に置いていないか
  3. React DevTools Profilerを実行する: 何が再レンダリングされているかを確認する

通常はここで 「高コストなサブツリーを memo() でラップする」 という手段が取られますが、コンパイラが自動化してくれない限り、これは手間のかかる作業です。

そこでmemoを使わずに解決できる2つのアプローチを見ていきましょう。

問題のあるコンポーネント構成

まず、レンダリングパフォーマンスに深刻な問題を抱えたコンポーネントの例を見てみます。

import { useState } from 'react';

export default function App() {
  let [color, setColor] = useState('red');
  return (
    <div>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <p style={{ color }}>Hello, world!</p>
      <ExpensiveTree />
    </div>
  );
}

function ExpensiveTree() {
  let now = performance.now();
  while (performance.now() - now < 100) {
    // 人工的な遅延:100ms何もしない
  }
  return <p>I am a very slow component tree.</p>;
}

何が問題か?

App コンポーネント内で color が変更されるたびに、App 全体が再レンダリングされます。これには、人工的に遅延させている <ExpensiveTree /> も含まれます。

ここで ExpensiveTreememo() で囲むこともできますが、もっとシンプルな解決策があります。

解決策1:ステートを下に移動する (Move State Down)

レンダリングされているコードをよく見ると、現在の color に関心があるのはツリーの一部だけであることがわかります。

export default function App() {
  let [color, setColor] = useState('red');
  return (
    <div>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <p style={{ color }}>Hello, world!</p>
      <ExpensiveTree />
    </div>
  );
}

そこで、その部分を Form コンポーネントとして切り出し、ステートをその内部(下層)に移動させます。

export default function App() {
  return (
    <>
      <Form />
      <ExpensiveTree />
    </>
  );
}

function Form() {
  let [color, setColor] = useState('red');
  return (
    <>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <p style={{ color }}>Hello, world!</p>
    </>
  );
}

結果

これで color が変更されても、再レンダリングされるのは Form コンポーネントだけになります。App は再レンダリングされないため、ExpensiveTree も影響を受けません。

解決策2:コンテンツを上に持ち上げる (Lift Content Up)

上記の解決策は、ステートが「高コストなツリー」の上位にある親要素で使用されている場合には機能しません。

例えば、color を親の <div> に適用したい場合です:

export default function App() {
  let [color, setColor] = useState('red');
  return (
    <div style={{ color }}>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <p>Hello, world!</p>
      <ExpensiveTree />
    </div>
  );
}

この場合、color を使用しない部分だけを別のコンポーネントに抽出することはできません。親の <div>ExpensiveTree を含んでいるためです。ここでこそ memo が必要に見えますが、別の手があります。

children プロップを活用する

答えは非常にシンプルです。App コンポーネントを2つに分割します。

  1. ColorPicker: color に依存する部分とステートを持つコンポーネント。
  2. App: color に関心のない部分を持ち、それを ColorPicker に渡すコンポーネント。
export default function App() {
  return (
    <ColorPicker>
      <p>Hello, world!</p>
      <ExpensiveTree />
    </ColorPicker>
  );
}

function ColorPicker({ children }) {
  let [color, setColor] = useState("red");
  return (
    <div style={{ color }}>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      {children}
    </div>
  );
}

なぜこれで解決するのか?

App コンポーネント内で、color に関心のない部分(ExpensiveTree を含む)は ColorPicker への JSX コンテンツ、つまり children プロップとして渡されています。

color が変更されると ColorPicker は再レンダリングされます。しかし、受け取っている children プロップ自体は、前回のレンダリング時と同じ参照を持っていますApp が再レンダリングされていないため)。

その結果、Reactはそのサブツリー(children の中身)を再訪せず、ExpensiveTree の再レンダリングは発生しません。

このパターンのメリット

memouseMemo を適用する前に、「変化する部分」と「変化しない部分」を分離できないか? 検討することに意味があります。

パフォーマンス以外のメリット

このアプローチの面白い点は、これらが本質的にはパフォーマンスのためのテクニックではないということです。children プロップを使ってコンポーネントを分割することは、通常、データフローを追いやすくし、ツリーを通じてバケツリレーされるプロップ(Prop Drilling)の数を減らすことにつながります。パフォーマンス向上は、その結果として得られる「おまけ」のようなものです。

まとめ

これらのテクニック(ステートを下に移動する、コンテンツを上に持ち上げる)は、memouseMemoを置き換えるものではなく、補完的なものです。

  1. まずはコンポーネントの構造を見直す(「変化する部分」を分離する)。
  2. それでも不十分な場合に、Profiler を使用して memo を適用する。
    ※ こちらはビルド時に自動で最適化を行ってくれて、手動のメモ化やパフォーマンスチューニングを大幅に減らしてくれるReact Compiler v1.0が2025年10月にリリースされたことにより、ほぼ不要となりました。

これは React のコンポジションモデル(組み合わせのモデル)の自然な構成であり、シンプルですが過小評価されがちなテクニックです。ぜひ memo() を書く前に、一度コンポーネントの構造を見直してみてください。

Discussion

Honey32Honey32

失礼します。元記事の公開日は「February 23, 2021」で、その後「Oct 7, 2025 」には、 React Compiler 1.0 がリリース されています。

末尾の説明文を使っていうと、「1. まずはコンポーネントの構造を見直す(「変化する部分」を分離する)。」までは正しい内容なのですが、その次の「2. それでも不十分な場合に、Profiler を使用して memo を適用する。」はほぼ不要になると見られます。

現在の読者へ誤解を与えるのを少しでも避けるには、記事が書かれた当時と現状の差がある旨が補足されてあるのが望ましい形だと思います。

「個人の学習メモ」ではなく「有益な情報の共有」という体裁を取っていらっしゃるので、重箱の隅をつつくような指摘になってしまい恐縮です。

Nao8Nao8

いえ、見直しが甘かったですmm
ご指摘ありがとうございます!訂正しました!