🕌

React レンダリング最適化(useMemo, useCallback, React.Memo)

2021/01/14に公開

useMemo, useCallback, React.memoの存在は知っていましたが、パフォーマンスの改善が必要だと思われる時点で導入しようと考えていました。最近少しずつでも適用していこうという考えで改めて整理したいと思います。

参照(Reference)

まず、参照という概念を理解する必要があります。以下の画像はpass by reference(参照渡し)とpass by value(値渡し)の違いを簡単に表現したものです。

pass by reference vs pass by value

pass by referenceは、片方にコーヒーを入れれば、もう片方にも同じようにコーヒーが満たされる。値を共有しているのであります。(Shallow Copy)
pass by valueは片方にコーヒーを入れても、もう片方には変化がないです。 それぞれ異なる値で存在します。(Deep Copy)

JavaScriptでプリミティブ型(String, Number, Bigint, Boolean, Undefined, Symbol)はpass by value, その他Object, Array等はpass by referenceとして理解してもらって良いです。

これをコードで確認してみます。

// String
let str1 = "Hello"
let str2 = str1
str2 = "World"

console.log(str1)  // "Hello"
console.log(str2)  // "World"
console.log(str1 === str2)  // false

// Object
let obj1 = { count: 1 }
let obj2 = obj1
obj2.count = 2

console.log(obj1)  // { count: 2 }
console.log(obj2)  // { count: 2 }
console.log(obj1 === obj2)  // true

Shallow Compare

Reactでリレンダリングが発生する場合は以下のとおりです。

  • stateに変化がある場合
  • 親コンポーネントがレンダリングされる場合
  • propsに変化がある場合
  • shouldComponentUpdateライフサイクル関数がtrueを返す場合
  • forceUpdateが実行される場合
    この中で1、2番の場合、Reactではshallow compareを通じてリレンダリングするかどうかを決めます。

shallow compareとは、参照タイプ(Object、Array など)の実際の内部値まで比較せずに同一参照かを比較することを意味する。

stateにpush、pop、spliceなどの原本を変形するメソッドを使用してはならない理由でもあります。

useMemo

以下のCodeSandboxでサンプルコードをご確認ください。簡単に平均値を計算して表示するコンポーネントです。

平均値はよく示していますが、数字を登録する時だけでなく、入力するたびにもgetAverage関数が呼び出されていることが分かります。

登録の場合ではない時は平均値を計算する必要はないので以下のようにuseMemoを追加して修正します。

// Average.js
... (省略)

const avg = useMemo(() => getAverage(list), [list])

return (
... (省略)
<div>
  <b>平均:</b> {avg}
</div>
... (省略)

これからは、list配列の要素が変わる時にのみgetAverage関数が呼び出されます。

useCallback

現在、子コンポーネントに渡すcallback関数をinline関数として使用しているとか、コンポーネント内で関数を生成しているのであればプログラムの挙動には問題ないですが、新しい関数参照を作り続けているのであります。

useCallback未適用

inline関数

以下のようにChildComponentのonClickに渡す関数としてinline関数を使うことになると、ChildComponentがレンダリングされるすべての時点で新たに同じ関数を作って使うことになる。

const ParentComponent = () => {
  return (
    <>
      <ChildComponent onClick={() => console.log('callback')}/>
      <ChildComponent onClick={() => console.log('callback')}/>
      <ChildComponent onClick={() => console.log('callback')}/>
      <ChildComponent onClick={() => console.log('callback')}/>
      <ChildComponent onClick={() => console.log('callback')}/>
	  ...
    </>
  );
};

// レンダリングされるChildComponentの数と同じ数のinline関数が生成されます。
const ChildComponent = ({onClick}) => {
  return <button onClick={onClick}>Click Me!</button>
};

local関数

inline関数よりは良い方式です。下記のような場合にはParentComponentがレンダリングされる時にのみonClick関数が再生成される。

const ParentComponent = () => {
  const onClick = () => {
    console.log('callback');
  };
  
  return (
    <>
      <ChildComponent onClick={onClick}/>
      <ChildComponent onClick={onClick}/>
      <ChildComponent onClick={onClick}/>
	  ...
    </>
  );
};

// ChildComponentが何度生成されても、propsに渡されるonClick関数は一度だけ生成されます。 
const ChildComponent = ({ onClick }) => {
  return <button onClick={onClick}>Click Me!</button>
};

useCallback適用

const ParentComponent = () => {
  const onClick = useCallback(() => {
    console.log('callback');
  }, []); 
  
  return (
    <>
      <ChildComponent onClick={onClick}/>
      <ChildComponent onClick={onClick}/>
      <ChildComponent onClick={onClick}/>
	  ...
    </>
  );
};

// ParentComponent, ChildComponentが何度レンダリングされてもpropsに渡されるonClick関数はいつも同じ参照の関数を返します。
const ChildComponent = ({ onClick }) => {
  return <button onClick={onClick}>Click Me!</button>
};

こうすれば、常に同じ値がChildComponentに渡されます。

しかし、ChildComponentでは引き続きリレンダリングが発生しているのでこれを解決するためにはReact.memoが必要である。

React.memo

React.memoはshouldComponentUpdateライフサイクル関数が基本として組み込まれた関数コンポーネントであると考えてもっらて良いです。

浅い比較演算を通じて同一の参照値のプロプが入ってくるなら、レンダリングを防止する。

React.memoを適用した最終コードは以下になります。

const ParentCompoenent = () => {
  const onClick = useCallback(() => {
    console.log('callback');
  }, []);
  
  return (
    <>
      <ChildCompoenent onClick={onClick}/>
      <ChildCompoenent onClick={onClick}/>
      <ChildCompoenent onClick={onClick}/>
	  ...
    </>
  );
};

// React.memo適用
const ChildCompoenent = React.memo(({ onClick }) => {
  return <button onClick={onClick}>Click Me!</button>
});

番外

useMemo? useCallback? どっちを使えば良い?

useCallbackは結局、useMemoで関数を返す場合により楽に使用できるHookであります。
String、Number、Objectのように一般値を再利用するためにはuseMemoを、関数を再利用するためにはuseCallbackを使用しましょう。
以下のコードは全く動作が同じであります。

useCallback(() => {
  console.log('hello world!');
}, [])

useMemo(() => {
  const fn = () => {
    console.log('hello world!');
  };
  return fn;
}, [])

useCallbackを使用するのにレンダリングがより発生する状況

useCallbackReact.memoを正しく書いたにもかかわらず、useCallbackを使うと親コンポーネントのレンダリングが2倍になる場合があります。
これはStrictModeと関連がある可能性があります。

対策: index.jsの<React.strictMode>を削除する。
参考URL

useMemoやuseCallbackは常に使った方が良い?

いいえ。useMemoとuseCallbackは既存の関数や値を返すか新しく作るか判断するロジックを動かすので頻繁に新しい関数や値が生成される場合はもしろパフォーマンスが下がる場合があります。

Quiz 1

useCallbackを使ってレンダリングを最適化してみてください。
Edit quiz-1

Quiz 2

先ほど平均値を計算して表示するコンポーネントをuseMemouseCallbackを使って最適化してみてください。
Edit average

Discussion