メモ化でReactのパフォーマンスチューンイングを行う

2022-03-13

2022-03-13

8 min read

React

メモ化とは

Reactでパフォーマンスチューニングを行う方法としてメモ化というものがあります。 メモ化を利用することで、コンポーネントのレンダリングを制御し、UXの向上に繋げることができます。言い換えると、無駄なレンダリングを無くして、動きを軽くするということです。これを適所に正しく用いることで、重たい処理がある画面でもサクサクした操作が期待できます。

再レンダリングについて

メモ化の具体的な内容に触れる前に、まずReactの再レンダリングについて考えたいと思います。

Reactで再レンダリングされるタイミングは、下記のような場合です。

  • stateが更新された時
  • propsが更新された時

また、再レンダリングされたコンポーネントの配下の子要素は再レンダリングされるので、たくさんのコンポーネントを組み合わせて作った画面はでは、このことを頭に入れておく必要があります。

ちなみにReactでは、コンポーネントベースで開発を行うことが多いと思います。コンポーネント設計についてはこちらの記事「Atomic Designを用いたReactのコンポーネント設計」で記述してるので、ご興味がある方はぜひ読んでみてください!

Reactでメモ化を行う方法

Reactでメモ化を行うには下記の関数を使用します。

  • memo(React.memo)
  • useCallback
  • useMemo

それでは使い方や挙動について1つずつ確認していきます。

memoとは

React.memoはコンポーネントのメモ化を行うことができます。

これを利用することで、親のコンポーネントがstateの変更などで再レンダリングされても、子のコンポーネントに影響がなければ再レンダリングを防ぐことができます(全文で触れた「再レンダリングされたコンポーネントの配下の子要素は再レンダリングされる」という場面です)。

以下サンプルコードです。

//親コンポーネント
export default function App() {
  const [value, setValue] = useState('')
  const onChange = (e)=>setValue(e.target.value)
  return (
    <div className="App">
      <input
        type='text'
        value={value}
        onChange={onChange}
      />
      <Children />
    </div>
  );
}

//子コンポーネント
const Children = () => {
  let data = []
  for(let i=0; i<3; i++){
    console.log(i)
    data.push(<p key={i}>{i}</p>)
  }
  return data
}

初期表示

reactmemo1.png

input入力後表示

reactmemo2.png

inputの値が入力される度に、関係のない子コンポーネントの値も際レンダリングされてしまっています。

ではReact.memoを使って再レンダリングを制御してみます。

//子コンポーネント
const Children = React.memo(() => {
  let data = []
  for(let i=0; i<3; i++){
    console.log(i)
    data.push(<p key={i}>{i}</p>)
  }
  return data
})

reactmemo3.png

inputの値が入力されるされても、再レンダリングされていないことが確認できました!

useCallbackとは

useCallbackは関数のメモ化を行うことができます。

それでは先程のコードを少し書き換えて、動きを見ていきます。

//親コンポーネント
export default function App() {
  const [value, setValue] = useState('')
  const onChange = (e)=>setValue(e.target.value)
  const onClick = ()=>alert('ok')
  return (
    <div className="App">
      <input
        type='text'
        value={value}
        onChange={onChange}
      />
      <Button onClick={onClick} />
    </div>
  );
}

//子コンポーネント
const Button = React.memo(({onClick}) => {
  for(let i=0; i<3; i++){
    console.log(i)
  }
  return(
    <button
      type="button"
      onClick={onClick}
    >ok</button>
  )
})

子コンポーネントをボタンのコンポーネントにして、親のコンポーネントからproprでonClick関数を渡しています。

input入力後表示

usecallback1.png

先ほどのReact.memoによって子コンポーネントを制御しているにも関わらず、inputの値が入力される度に再レンダリングされてしまっています。

これは親から子コンポーネントに渡しているonClick関数が、レンダリングの度に再生成されているためです。useCallbackを用いて関数のメモ化を行うことで、子コンポーネントの再レンダリングを防ぐことができます。

ではuseCallbackを使って再レンダリングを制御してみます。

const onClick = useCallback(()=>alert('ok'),[])

変更内容はonClick関数をuseCallbackを使って定義するだけです。inputを入力してみると、再レンダリングが制御できてることが確認できます!

ちなみに第二引数には依存関係を指定することができます。

useMemoとは

useMemoは変数のメモ化を行うことができます。

今回はuseMemoの動きの違いを比較できるように、下記のようなサンプルコードを記述します。

export default function App() {
  const [count, setCount] = useState(0)
  const onClick = ()=>setCount(count + 1)
  const result1 = count + 10
  const result2 = React.useMemo(()=>count + 10,[])
  const result3 = React.useMemo(()=>count + 10,[count])

  return (
    <div className="App">
      <button onClick={onClick}>+</button>
      <p>result1:10 + {count} = {result1}</p>
      <p>result2:10 + {count} = {result2}</p>
      <p>result3:10 + {count} = {result3}</p>
    </div>
  );
}

カウントボタンが押下されると、+1されていく簡単なプログラムです。

カウントボタン押下後

usememo1.png

違いをまとめると下記のようになります。

  • result1
    • レンダリングのたびに再計算される
  • result2
    • 初回レンダリングの時のみ計算される
  • result3
    • 依存関係に指定しているcountの値が変更される時計算される

重い計算処理などは、useMemoを使用してメモ化しておくことで、必要な時のみ計算されるように制御できます。

まとめ

メモ化はパフォーマンスの向上に繋がりますが、全てのコンポーネントをメモ化制御する必要はないと思います。

メモ化の内部的な動きは、レンダリングの前後の値を比較して、差分がない場合に不要なレンダリングを抑制するものです。メモ化を行う状況によっては、この差分の比較が大変であれば、パフォーマンスの低下につながる場合もあります

個人的には最初からこの辺りまで細かく考えて実装するより、重そうな処理のとこや、実際に動かしてみて重いと感じたとこをメモ化していくくらいでいいのかなと思っています。