9
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?

More than 1 year has passed since last update.

【React】パフォーマンスの最適化(useCallback)

Last updated at Posted at 2023-08-31

Reactのパフォーマンスについての備忘録です。

再レンダリングを防ぐための関数

  • React.memo
  • useCallback
  • useMemo

React.memo

受け取ったpropsの値が同じであればstateの差分を検知し、再レンダリングをスキップします。
React.memoで囲むと、囲まれたコンポーネントのpropsに変更がない場合は、配下のコンポーネントは再レンダリングされません。

一方で、React.memoで囲まない場合は、propsの有無の変更にかかわらず、全て再レンダリングの対象になります。

関数がpropsに渡る場合

コンポーネント内で定義した関数は再レンダリングのたびに再生成(新しく定義)されます。
そのため異なる関数としてみなされ、React.memoで囲っていても、propsを渡すと再レンダリングの対象になってしまいます。

コンポーネントA
const handleClick = ()=> {・・・}

⇅ 再レンダリング前後で異なる関数

コンポーネントA(再レンダリング)
const handleClick = ()=> {・・・}

React.memoだけでは再レンダリングを防げません。

それを解決するのが・・・↓

useCallback

コンポーネント内で定義した"関数"をメモして再利用し、レンダリングの度に生成されることを防ぎます。

子コンポーネントに関数を渡している場合に、不要な再レンダリングを防ぐことができます。

そのため、その関数に対して変更がない場合には、React.memoの機能が有効化されるので、子コンポーネントを不要な再レンダリングから防ぎます。

コンポーネント以外にも様々な値をメモ化するには・・・??↓

useMemo

コンポーネントだけではなく、値をメモすることが可能。コストの高い処理などをメモ化します。
しかし、useMemo自体の実行にもコストがかかるため、重い処理にのみ使用すること。

※何でもかんでもuseMemo化してはいけない
┗ 処理自体にも計算コストがかかるため、重い処理にのみ使用します。

コンポーネントを再レンダリングから防ぐことによってパフォーマンスを防ぐことができます。

さらにuseCallbackについて学ぶ

useCallbackは、再レンダリングの間に関数定義をキャッシュできるようにする React フック。

useCallback(fn, dependencies)

パラメーター
fn: キャッシュする関数の値。任意の引数を受け取り、任意の値を返すことができます。React は、最初のレンダリング中に関数を返します (呼び出しではありません!)。dependencies次回のレンダリングでは、前回のレンダリング以降に変更がなかった場合、React は同じ機能を再度提供します。それ以外の場合は、現在のレンダリング中に渡した関数が提供され、後で再利用できるように保存されます。React は関数を呼び出しません。関数が返されるので、それをいつ呼び出すか、呼び出すかどうかを決定できます。

dependencies: コード内で参照されるすべてのリアクティブ値のリストfn。リアクティブ値には、props、state、およびコンポーネント本体内で直接宣言されたすべての変数と関数が含まれます。リンターがReact 用に構成されている場合、すべての reactive 値が依存関係として正しく指定されているかどうかが検証されます。依存関係のリストには一定数の項目があり、 のようにインラインで記述する必要があります[dep1, dep2, dep3]。React は、比較アルゴリズムを使用して、各依存関係を以前の値と比較しますObject.is。

React公式ドキュメント

簡単なコードで解説

Exsample.js
(親コンポーネント)
import React, { useState } from "react";
import Child from "./Child";

const Example = () => {
  console.log("Parent render");
  
  const [countA, setCountA] = useState(0);
  const [countB, setCountB] = useState(0);

  const clickHandler = () => {
    setCountB((pre) => pre + 1);
  }

  return (
    <div className="parent">
      <div>
        <h3>親コンポーネント領域</h3>
        <div>
          <button
            onClick={() => {
              setCountA((pre) => pre + 1);
            }}
          >
            ボタンA
          </button>
          <span>親のstateを更新</span>
        </div>
      </div>
      <div>
          <p>ボタンAクリック回数{countA}</p>
      </div>
      <Child countB={countB} onClick={clickHandler}/>
    </div>
  );
};

export default Example;
Child.js
(子コンポーネント)
import { memo } from "react";

const ChildMemo = memo(({ countB, onClick }) => {
  console.log("%cChild render", "color: red;");

  return (
    <div className="child">
      <h2>子コンポーネント領域</h2>
      <div>
        <button
          onClick={onClick}
        >
          ボタンB
        </button>
        <span>子のpropsに渡すstateを更新</span>
      </div>
      <span>ボタンBクリック回数{countB}</span>
    </div>
  );
});

export default ChildMemo;

親コンポーネントにボタンAがあります。
子コンポーネントにボタンBがあり、親のコンポーネントにボタンを押した際の挙動に関する関数を定義しています。

propsでonClickを渡します。

ボタンAを押すと、stateが更新されます。
親コンポーネント、子コンポーネントでそれぞれ追記していたconsole.logをコンソールで確認すると、両方呼び出されています。

このことから、親コンポーネントにあるボタンAを押すと、親と子が両方再レンダリングされていることがわかります。

Child.jsでpropsを受け渡す際にmemoで囲っていても、子コンポーネントが再レンダリングされてしまっていることが分かります。

スクリーンショット 2023-08-31 14.36.16.png

理由は、親コンポーネントのconst clickHandler関数が、親コンポーネントが再レンダリングされる度に定義されるので、<Child countB={countB} onClick={clickHandler}/> のpropsで受け渡されたclickHandlerと、改めて再レンダリングの際に定義されたconst clickHandler関数は別物になってしまうからです。

コンポーネントA
const clickHandler = () =>  {・・・}

⇅ 再レンダリング前後で異なる関数

コンポーネントA(再レンダリング)
const clickHandler = () =>  {・・・}

そこで、const clickHandler関数を、useCallbackで囲むことで、上記の問題を解決できます。

  const clickHandler = useCallback(() => {
    setCountB((pre) => pre + 1);
  },[])

ここでの注意点としては、第二引数には空の配列を渡す必要があります。
第二引数は依存配列と呼ばれます。

useCallbackで囲んだ状態でボタンAを押すと、先ほどのように子コンポーネントは再レンダリングされなくなります。

useCallbackを使うことによって、毎回関数を定義しているのではなく、一度useCallbackに渡された関数は、React内部に保持されることになります。

再レンダリングのタイミングでは、このReact内部で保持した関数をこのClickHandler関数に返す形になるため、一番最初に定義した関数を使う回すことになります。

このように子コンポーネントで関数を受け取るときは、親コンポーネントで渡したい関数をuseCallbackで囲むと良いでしょう。

第二引数の依存配列について

依存配列に含めたstateの値が更新されると、useCallbackで渡した関数があらためてReact内部で上書きされ、最新のstateの値が使用できることになります。

上記の例にcountAを第二引数の依存配列に含めると、カウントの値が変更されるたびに関数が新しくReact内部に保持されるので、レンダリングのたびに違う関数が子コンポーネントのpropsに渡ることになります。

そのため、「ボタンA」を押すと、Childコンポーネントの再レンダリングが発生されることになります。

  const clickHandler = useCallback(() => {
    setCountB((pre) => pre + 1);
  },[countA])

依存配列にcountAを含めて、ボタンAを押した場合

(親コンポーネントに「ボタンA」があっても再レンダリングされる。)

依存配列にcountAを含めず、ボタンAを押した場合

参考

9
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
9
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?