5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【React】useMemoとuseCallbackの違いを理解してパフォーマンス最適化する

Last updated at Posted at 2024-07-25

疑問

Reactでパフォーマンス最適化を行う際に「useMemo」と「useCallback」というものが登場するが、これらの違いって何...?

結論

Reactでは、「特定の場合にしか必要のない処理なのに、コンポーネントの再レンダリングによって無駄にその処理が再実行されてしまう」ということがたまに発生します。

そこでReactでは、「この条件を満たす場合に だけ 処理を再実行せい」という指定をすることができます。これがいわゆる「メモ化」と呼ばれるものです。

useMemouseCallbackはこの「メモ化」を行うための手段で、「何に対して再実行の条件をつけるか」という点が異なります。

⭐️ useMemoは、「処理の再計算」に条件をつけることができます。

⭐️ useCallbackは、「関数の再定義」に条件をつけることができます。

詳しく解説

Reactコンポーネントではリアクティブなデータを定義する際にuseStateを使用します。そのデータが更新されると、コンポーネントの再レンダリングが実行されます。

以下のようなコンポーネントを例とすると、テキストフォームの入力値でStateのnameを更新しているので、フォームへの入力がある度にブラウザのコンソールに「再レンダリング」と出力されます。

この挙動は時に画面が重くなる原因となります。

export const HelloWorld = () => {
  const [name, setName] = useState<string>(""); // Stateを定義

  console.log("再レンダリング"); // nameが更新される度にログが出力される

  return (
    <div>
      <input
        type="text"
        onChange={(e) => setName(e.target.value)} // 入力値でStateの値nameを更新
        value={name}
      />
    </div>
  );
};

useMemoとは何か / 使い所

以下のようなコンポーネントがあるとします。とても重い処理を内部で行なっている関数 heavyCalc の結果を doubleCount という変数に格納しています。

このheavyCalc関数内で使用している(依存している)Stateの値はcountだけなので、countが更新された場合のみ「const doubleCount = heavyCalc();」が再実行されて欲しいのですが、

先述の例でお話した通り、ReactではStateの値に更新があった場合にはコンポーネント内の処理が再実行される為、フォームに値が入力されてheavyCalc関数に全く関係のないState name が更新された場合にも、毎回「const doubleCount = heavyCalc();」が再実行されてしまいます。


export const HelloWorld = () => {
  const [name, setName] = useState<string>("");
  const [count, setCount] = useState<number>(0);

  const heavyCalc = (): number => {
    let x = 0;

    for (let i = 0; i < 100000000; i++) {
      x++;
    }

    return count * 2;
  };

  const doubleCount = heavyCalc(); // レンダリングされる度に実行される

  return (
    <div>
      <input
        type="text"
        onChange={(e) => setName(e.target.value)}
        value={name}
      /><br/><br/>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      <p>Double count: {doubleCount}</p>
    </div>
  );
};

その結果、テキストフォームに値を入力するだけでも処理時間が長くなり、かなり画面がもっさりしてしまいます。

Screenshot 2024-07-25 at 8.54.58.png

そこで使用するのが「useMemo」です。

useMemoは、「処理の再計算」に条件をつけることができます。

heavyCalc(); という処理の再計算に条件をつけることでパフォーマンス最適化を図ることができます。

使い方は簡単です。第一引数に「再計算されてほしくない処理」を指定して、第二引数に「コイツが更新されたら再計算していいよ。なヤツ」を指定します。

再計算されてほしくない処理は heavyCalc(); で、その処理が依存する値(コイツが更新されたら再計算していいよ。なヤツ)は count なので、useMemo(() => heavyCalc(), [count])のような形になります。

export const HelloWorld = () => {
  const [name, setName] = useState<string>("");
  const [count, setCount] = useState<number>(0);

  const heavyCalc = (): number => {
    let x = 0;

    for (let i = 0; i < 100000000; i++) {
      x++;
    }

    return count * 2;
  };

  const doubleCount = useMemo(() => heavyCalc(), [count]); // countが更新された時だけ再計算

  return (
    <div>
      <input
        type="text"
        onChange={(e) => setName(e.target.value)}
        value={name}
      />
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      <p>Double count: {doubleCount}</p>
    </div>
  );
};

そうすると、以下のようにフォームに文字列を入力した際の処理速度が 122.3ms から 0.2ms と、かなり早くなりました。画面のもっさり感は跡形もなくなったと思います!👏

count が更新された際には、適切に heavyCalc() が再計算されていることも確認できると思います。

Screenshot 2024-07-25 at 8.53.38.png

このように、useMemo は処理の再計算に条件をつけることができます。

useCallbackとは何か / 使い所

useCallbackを理解する際には、React内のDOMツリーにおけるルールについて理解しておくとスムーズに理解が進みます。

Reactでは、Stateの値の更新などによって再レンダリングが実行された場合、その子コンポーネントにあたるものも、再レンダリングが実行されます。

これが時に無駄な挙動となりうるので、コンポーネント自体をメモ化することでこれを回避することができます。

Screenshot 2024-07-25 at 8.40.31.png

const Child = () => {
  return <p>子コンポーネント</p>;
};

export const Parent = () => {
  const [name, setName] = useState<string>("");

  return (
    <div>
      <input
        type="text"
        onChange={(e) => setName(e.target.value)}
        value={name}
      />
      <Child />
    </div>
  );
};

以下のように、テキストフォームに入力によってState name が更新された際、Childコンポーネントも再レンダリングが行われていることが分かります。

Screenshot 2024-07-25 at 8.51.59.png

ここで使用するのが、「コンポーネントのメモ化」です。

Reactで提供されているmemoを使用することでコンポーネントをメモ化することができます。

Childコンポーネントをmemoで囲ってあげるだけで完了です。

const Child = React.memo(() => {
  return <p>子コンポーネント</p>;
});

export const Parent = () => {
  const [name, setName] = useState<string>("");

  return (
    <div>
      <input
        type="text"
        onChange={(e) => setName(e.target.value)}
        value={name}
      />
      <Child />
    </div>
  );
};

そうすると、テキストフォームに入力によってState name が更新されたとしても、Childコンポーネントも再レンダリングは行われず、Parentのみ適切に再レンダリングが実行されていることが分かります。

Screenshot 2024-07-25 at 8.59.04.png

今回の場合、Childがとてもシンプルなコンポーネントだった為、メモ化を行わなくても目に見えてパフォーマンスが落ちるということはありませんが、たとえばChildコンポーネントに変更してあげると、メモ化の有無によってパフォーマンスに大きく違いが現れます。

const Child = () => {
  let x = 0;

  for (let i = 0; i < 100000000; i++) {
    x++;
  }

  return <p>子コンポーネント</p>;
};

では本題の、useCallbackはいつ使うのか。という件です。

もちろんですが、コンポーネントのメモ化もuseMemo同様に何らかの条件に基づいて再レンダリングは行われます。その条件というのが、「コンポーネント内の状態に変化があった場合」または「propsに変更があった場合」です。

この「propsに変更があった場合」がポイントです。

Reactでは、以下のように子コンポーネントのprops(onChange)として親コンポーネント側で定義された関数(handleSearch)を渡すことがあります。

interface ChildProps {
  onChange: (text: string) => void;
}

const Child = React.memo(({ onChange }: ChildProps) => {
  let x = 0;

  for (let i = 0; i < 100000000; i++) {
    x++;
  }
  return <input onChange={(e) => onChange(e.target.value)} />;
});

export const Parent = () => {
  const [name, setName] = useState<string>("");
  const [searchWord, setSearchWord] = useState<string>("");

  const handleSearch = (text: string) => {
    setSearchWord(text);
  };

  return (
    <div>
      <input
        type="text"
        onChange={(e) => setName(e.target.value)}
        value={name}
      />
      <p>Search word: {searchWord}</p>
      <Child onChange={handleSearch} />
    </div>
  );
};

Childコンポーネントはメモ化されているので、Parentが再レンダリングされたとしてもChildは再レンダリングされないと思いきや、
テキストフォームに入力によってState name が更新された際に、レンダリングが重いChildも再レンダリングされてしまい、画面がもっさりしてしまいます...

Screenshot 2024-07-25 at 9.32.10.png

これは、Parentが更新された際にhandleSearch再定義されていることが原因です。

たとえhandleSearchの内部処理が全く同じだとしても、React上では再定義が行われた時点で、再定義前の関数とは「異なる」という評価がされます。

これによりChildに渡しているprops onChange に更新があったと見なされ、Childが再レンダリングされてしまうということなのです。

こういった、不要な関数の再定義によるパフォーマンス低下の対策として用意されているものが、useCallbackなのです!

使い方は簡単です。第一引数に「再定義されてほしくない関数」を指定して、第二引数に「コイツが更新されたら再定義していいよ。なヤツ」を指定します。

大抵の場合、再定義はされてほしくないので第二引数には空配列を指定することがほとんどです。

interface ChildProps {
  onChange: (text: string) => void;
}

const Child = React.memo(({ onChange }: ChildProps) => {
  let x = 0;

  for (let i = 0; i < 100000000; i++) {
    x++;
  }
  return <input onChange={(e) => onChange(e.target.value)} />;
});

export const Parent = () => {
  const [name, setName] = useState<string>("");
  const [searchWord, setSearchWord] = useState<string>("");

  const handleSearch = useCallback((text: string) => {
    setSearchWord(text);
  }, []);

  return (
    <div>
      <input
        type="text"
        onChange={(e) => setName(e.target.value)}
        value={name}
      />
      <p>Search word: {searchWord}</p>
      <Child onChange={handleSearch} />
    </div>
  );
};

そうすると、テキストフォームに入力によってState name が更新されParentが再レンダリングされたとしても、Childは再レンダリングされずに適切にメモ化が維持され、画面のもっさり感も解消されます!👏

Screenshot 2024-07-25 at 9.33.58.png

このように、useCallback は関数の再定義に条件をつけることができます。

5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?