🔴

Reactパフォーマンス向上の鍵:memo/useCallbackの使い方と実例

2023/03/21に公開

Reactのレンダリングを最適化するために用いられるmemoについて解説します。

Reactの再レンダリングが発生する条件

Reactの再レンダリングは、主に以下の条件で発生します。

  • コンポーネントのstateが変更されたとき
  • コンポーネントのpropsが変更されたとき
  • 親コンポーネントが再レンダリングされたとき
  • コンポーネントの内部で使用されるcontextが変更されたとき
    これらの条件が満たされると、Reactはコンポーネントの再レンダリングを実行し、変更があった部分だけを効率的に更新します。

不要な再レンダリングを防ぐためにmemoを使用する

再レンダリングが発生する条件で見たように、親コンポーネントが更新されるとそれに付随して子コンポーネントも再レンダリングされてしまいます。
親コンポーネントの更新内容に紐づいて子コンポーネントにも変更を与える場合は再レンダリングしたも良いのですが、親コンポーネントと子コンポーネントが全く別の役割を果たしている場合は無駄に再レンダリングが走るので、不要な処理が入ることで重くなってしまいますね。
そのような場合にmemoを使うことで回避しましょう。

React.memo()は、関数コンポーネントのレンダリング結果をメモ化して、再レンダリングの回数を削減するための高階関数です。React.memo()を使用することで、コンポーネントのpropsが変更されていない場合に再レンダリングをスキップすることができます。

React.memo()は、関数コンポーネントを引数に取り、メモ化されたコンポーネントを返します。メモ化されたコンポーネントは、propsが変更されない限り再レンダリングがスキップされます。これにより、不要な再レンダリングを防ぎ、アプリケーションのパフォーマンス向上につながります。

memoの仕組みを理解するためのコード例

まずはmemoを使用していない場合のコード例を見てみましょう。

import React, { useState } from "react";

const Child: React.FC<{ count: number }> = ({ count }) => {
  console.log("Child component rendered");
  return <p>Child Count: {count}</p>;
};

const Parent: React.FC = () => {
  const [parentCount, setParentCount] = useState(0);
  const [childCount, setChildCount] = useState(0);

  return (
    <div>
      <button onClick={() => setParentCount(parentCount + 1)}>Increment Parent Count</button>
      <p>Parent Count: {parentCount}</p>
      <button onClick={() => setChildCount(childCount + 1)}>Increment Child Count</button>
      <Child count={childCount} />
    </div>
  );
};

Increment Parent Countのボタンを押したら、変更して欲しいのはParentコンポーネントだけですが、子コンポーネントであるChildコンポーネントも再レンダリングされてしまいます。
実際にIncrement Parent Countボタンをクリックすると子コンポーネントが再レンダリングされていることが分かります。(子コンポーネントにconsole.logなどを入れて確認してみてください)
次にmemoを使用したコード例です。

import React, { useState, memo } from "react";

const Child: React.FC<{count: number}> = memo(({ count }) => {
  console.log("Child component rendered");
  return <p>Child Count: {count}</p>;
});

export const Parent: React.FC = () => {
  const [parentCount, setParentCount] = useState(0);
  const [childCount, setChildCount] = useState(0);

  return (
    <div>
      <button onClick={() => setParentCount(parentCount + 1)}>Increment Parent Count</button>
      <p>Parent Count: {parentCount}</p>
      <button onClick={() => setChildCount(childCount + 1)}>Increment Child Count</button>
      <Child count={childCount} />
    </div>
  );
};

memoを使用したコード例では、子コンポーネントのpropsが変更されていない場合、再レンダリングがスキップされます。
memoの使用方法はシンプルでmemoを利用したいコンポーネントをmemo()で囲むだけで良いです。
実際にParentコンポーネントに影響を与えるIncrement Parent Countボタンをクリックしても子コンポーネントは再レンダリングされていないことが分かるかと思います。

効果的なmemoの使い道

memo()は特に大規模なアプリケーションや多くの子コンポーネントを持つアプリケーションでパフォーマンスの向上が期待できます。ただし、すべてのコンポーネントでReact.memo()
を無闇に使用すると、メモリ消費が増加することがあるため、適切な場所で使用することが重要です。

memo()を使用する際のベストプラクティスは、以下の通りです。

  • 再レンダリングの回数が多く、パフォーマンスに影響が出るコンポーネントで使用する
  • propsの変更が少なく、再レンダリングが不要なコンポーネントで使用する
  • 必要に応じて、カスタム比較関数を使用して、特定のpropsの変更を無視することもできます

不要な再レンダリングを防ぐためのuseCallback

useCallbackは、関数のメモ化(キャッシュ)を行うためのフックです。
関数が変更されず、依存配列の値が変更されない限り、同じ関数インスタンスを返すことで、不要な再レンダリングを防ぎます。特に関数プロップを持つ子コンポーネントの再レンダリングを制御する際に有用です。
分かりやすく具体例で解説します。
memoを使用していても当然ながらpropsに変更が入った場合は再レンダリングが発生します。
子コンポーネントに親コンポーネントに関数を渡している場合は毎回propsに変更が入ったと解釈されてしまいます。

import React, { useState } from "react";

type ChildProps {
  onClick: () => void;
}

const Child: React.FC<ChildProps> = ({ onClick }) => {
  console.log("Child component rendered");
  return <button onClick={onClick}>Click me</button>;
};

const Parent: React.FC = () => {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <Child onClick={handleClick} />
    </div>
  );
};

上記の例ではParentコンポーネントから子コンポーネントにhandleClickという関数を渡してあげています。これが新規でpropsに加わると判断されてしまうので、再レンダリングの条件のコンポーネントのpropsが変更されたときに該当し、再レンダリングが発生してしまうわけです。
関数自体に変更がないのであればuseCallbackを用いて不要な再レンダリングを防ぎましょう

useCallbackの仕組みを理解するためのコード例

const memoizedFunction = useCallback(
  () => {
    // 関数本体
  },
  [dependency1, dependency2, ...]
);

memoの時と似ているのですが上記のように子コンポーネントに渡す関数をuseCallbackで囲ってあげます。
第1引数には、メモ化したい関数を渡します。
第2引数には、関数内で使用される依存性の配列を渡します。依存性の配列の値が変更されると、新しい関数インスタンスが作成されます。

import React, { useState, useCallback, memo } from "react";

type ChildProps {
  onClick: () => void;
}

const Child: React.FC<ChildProps> = memo(({ onClick }) => {
  console.log("Child component rendered");
  return <button onClick={onClick}>Click me</button>;
});

const Parent: React.FC = () => {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    setCount(count + 1);
  }, [count]);

  return (
    <div>
      <p>Count: {count}</p>
      <Child onClick={handleClick} />
    </div>
  );
};

useCallbackを使用したコード例では、handleClick関数がメモ化されており、依存配列の値(この場合はcount)が変更されない限り、同じ関数インスタンスが返されます。これにより、Childコンポーネントの不要な再レンダリングが防がれます。
useCallbackReact.memo()を組み合わせることで、関数プロップを持つコンポーネントのパフォーマンスを向上させることができます。

Discussion