【React初心者向け】React.memoで不要な再レンダリングを防ぐ!実践的なパフォーマンス最適化方法を解説

とまだ
13 分で読めます
Reactmemoパフォーマンス初心者向けフロントエンド

こんにちは、とまだです。

Reactでアプリを作っていて、「なんだか動作が重いな...」と感じたことはありませんか?

私も最初の頃は、コンポーネントがやたら再レンダリングされて、画面の動きがもっさりしてしまう経験がありました。特にリスト表示で100件とか表示していると、1件更新しただけで全部描き直されて「なんで?」って思ったものです。

今回は、そんな悩みを解決してくれる React.memo について、実際の使用例とともにわかりやすく解説します!

React.memoってなに?

React.memoを簡単に説明すると、「変更がない部分は再描画をスキップしてくれる機能」です。

例えば、大きなオフィスの会議室を想像してみてください。 会議の途中で新しい資料が配られたとき、全員が席を立ち上がって資料を取りに行く必要はありませんよね。必要な人だけが動けばいいはずです。

React.memoも同じで、propsに変更がなければ前回の結果を再利用してくれるんです。つまり、「変更がないなら無駄に動く必要なし!」ということです。

基本的な使い方

React.memoの使い方はとってもシンプルです。

import React from "react";

const DisplayText = React.memo(function DisplayText({ message }) {
  console.log("DisplayText rendered");
  return <p>{message}</p>;
});

export default DisplayText;

コンポーネントを React.memo() でラップするだけで、propsが同じ値なら再レンダリングをスキップしてくれます。

めちゃくちゃ簡単ですよね!

いつReact.memoを使うべき?

React.memoが特に効果を発揮するのは、大量のデータを扱ったり、頻繁に更新が起きる画面です。

リスト表示での活用

ショッピングサイトの商品一覧を想像してみてください。 100個の商品があって、そのうち1個だけ在庫が変わったとします。

React.memoを使わない場合、全ての商品カードが再レンダリングされてしまいます。これは明らかに無駄ですよね。

// React.memoを使わない場合(非効率)
function ProductCard({ product }) {
  console.log(`商品 ${product.id} がレンダリングされました`);
  return (
    <div>
      <h3>{product.name}</h3>
      <p>価格: {product.price}</p>
      <p>在庫: {product.stock}</p>
    </div>
  );
}

// React.memoを使った場合(効率的)
const ProductCard = React.memo(function ProductCard({ product }) {
  console.log(`商品 ${product.id} がレンダリングされました`);
  return (
    <div>
      <h3>{product.name}</h3>
      <p>価格: {product.price}</p>
      <p>在庫: {product.stock}</p>
    </div>
  );
});

React.memoを使えば、在庫が変わった商品だけが再レンダリングされるようになります。

私も以前、ECサイトの商品一覧で同じような問題に遭遇して、React.memoを導入したら劇的に動作が軽くなった経験があります。

ダッシュボード画面での活用

管理画面のダッシュボードも良い例です。

リアルタイムで更新される売上グラフと、ほとんど変わらないユーザー情報パネルがあるとします。ユーザー情報パネルをReact.memoでラップしておけば、売上データが更新されてもユーザー情報は再レンダリングされません。

これで「なんでユーザー情報まで再描画されるの?」という無駄をなくせます。

React.memoの注意点

React.memoは便利ですが、使う際に気をつけたいポイントがあります。

オブジェクトや配列の問題

以下のコードを見てください。

function App() {
  const [count, setCount] = useState(0);

  // 毎回新しいオブジェクトが作られる
  const user = { name: "田中", age: 25 };

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        カウント: {count}
      </button>
      <UserInfo user={user} />
    </div>
  );
}

const UserInfo = React.memo(function UserInfo({ user }) {
  console.log("UserInfo がレンダリングされました");
  return <p>{user.name}さん({user.age}歳)</p>;
});

このコードだと、React.memoを使っても毎回 UserInfo が再レンダリングされてしまいます。

なぜかというと、user オブジェクトが毎回新しく作られているからです。見た目は同じでも、JavaScriptでは「別のもの」として扱われてしまうんです。

これは私も最初にハマったポイントで、「React.memo使ってるのに効いてない!」って悩みました。

解決策:useMemoを組み合わせる

この問題は useMemo を使って解決できます。

function App() {
  const [count, setCount] = useState(0);

  // useMemoで値を固定
  const user = useMemo(() => ({
    name: "田中",
    age: 25
  }), []); // 空の依存配列なので、一度だけ作成される

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        カウント: {count}
      </button>
      <UserInfo user={user} />
    </div>
  );
}

これで、UserInfo は本当に必要な時だけ再レンダリングされるようになります。

関数を渡す場合の注意点

関数をpropsで渡す場合も同じ問題が起きます。

function App() {
  const [count, setCount] = useState(0);

  // 毎回新しい関数が作られる
  const handleClick = () => {
    console.log("ボタンがクリックされました");
  };

  return (
    <div>
      <p>カウント: {count}</p>
      <button onClick={() => setCount(count + 1)}>増加</button>
      <ChildButton onClick={handleClick} />
    </div>
  );
}

const ChildButton = React.memo(function ChildButton({ onClick }) {
  console.log("ChildButton がレンダリングされました");
  return <button onClick={onClick}>子ボタン</button>;
});

この場合は useCallback を使って関数を固定します。

function App() {
  const [count, setCount] = useState(0);

  // useCallbackで関数を固定
  const handleClick = useCallback(() => {
    console.log("ボタンがクリックされました");
  }, []); // 空の依存配列なので、関数は一度だけ作成される

  return (
    <div>
      <p>カウント: {count}</p>
      <button onClick={() => setCount(count + 1)}>増加</button>
      <ChildButton onClick={handleClick} />
    </div>
  );
}

ただし、useCallbackuseMemo を使いすぎると、逆にパフォーマンスが悪くなることもあります。本当に必要な場所だけで使いましょう。

実践的な使い分けのコツ

私の経験から、React.memoを使うべき場面とそうでない場面をまとめました。

使うべき場面

大量のリストアイテムを表示する商品一覧やユーザー一覧では効果抜群です。

重い計算を含むコンポーネントのグラフやチャートでも威力を発揮します。

頻繁に更新される親の子コンポーネントで、リアルタイム更新画面の一部などにも有効です。

使わなくても良い場面

シンプルなコンポーネントのボタンやテキスト表示では、そもそもレンダリングコストが低いので効果が薄いです。

必ず毎回変わるコンポーネントの現在時刻表示などでは、メモ化しても意味がありません。

親と一緒に更新されるコンポーネントのフォームの入力項目なども、一緒に更新されるのが自然なので不要です。

実際の改善例

最後に、実際にReact.memoでパフォーマンスが改善された例を見てみましょう。

// 改善前:全てのTodoアイテムが毎回再レンダリング
function TodoApp() {
  const [todos, setTodos] = useState([]);
  const [newTodo, setNewTodo] = useState("");

  const addTodo = () => {
    setTodos([...todos, { id: Date.now(), text: newTodo, done: false }]);
    setNewTodo("");
  };

  const toggleTodo = (id) => {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, done: !todo.done } : todo
    ));
  };

  return (
    <div>
      <input
        value={newTodo}
        onChange={(e) => setNewTodo(e.target.value)}
      />
      <button onClick={addTodo}>追加</button>
      {todos.map(todo => (
        <TodoItem key={todo.id} todo={todo} onToggle={toggleTodo} />
      ))}
    </div>
  );
}

// 改善後:変更されたTodoアイテムだけ再レンダリング
const TodoItem = React.memo(function TodoItem({ todo, onToggle }) {
  console.log(`Todo ${todo.id} がレンダリングされました`);

  const handleToggle = useCallback(() => {
    onToggle(todo.id);
  }, [todo.id, onToggle]);

  return (
    <div>
      <input
        type="checkbox"
        checked={todo.done}
        onChange={handleToggle}
      />
      <span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
        {todo.text}
      </span>
    </div>
  );
});

この改善により、100個のTodoアイテムがあっても、1個だけチェックした時に他の99個は再レンダリングされなくなります。

まとめ

React.memoは、不要な再レンダリングを防いでパフォーマンスを改善してくれる便利な機能です。

特に大量のデータを扱うリストや、頻繁に更新される画面で効果を発揮します。

ただし、オブジェクトや関数をpropsで渡す場合は、useMemouseCallback と組み合わせて使う必要があります。

全てのコンポーネントに適用するのではなく、本当に必要な場所を見極めて使うことが大切ですね。

もし「アプリの動作が重いかも」と感じることがあれば、ぜひReact.memoを試してみてください!きっと違いを実感できるはずです。