サムネイル

Reactのパフォーマンスチューニングについて勉強したところをまとめてみた

Reactにはパフォーマンスを最適化する為のAPIが用意されています。具体的にはReact.memo,useCallback,useMemoがあります。React.memoではコンポーネントをメモ化し、 useCallBackでは関数(コールバック関数)をメモ化。useMemoは値(関数の結果)のメモ化といった役割があります。

用語解説

メモ化とは、計算結果を保持し、それを再利用する手法のことです。メモ化によって各コンポーネントで都度計算する必要がなくなるため、パフォーマンスの向上が期待できます。


コールバック関数とは、ある関数を呼び出すときに引数に指定する別の関数のこと。


Reactのレンダリングとは、コンポーネントの本体である関数を評価(実行)すること

Reactの再レンダリングの条件

まず前提知識として、Reactの再レンダリングの条件を見ていきます。

  1. propsの変更
  2. stateの変更
  3. contextの変更
  4. forceUpdate()を使用しての、強制的な再レンダリング
  5. 親コンポーネントの再レンダリング

などが挙げられます。

useMemo

useMemoは値(関数の結果)を保持するためのhooksです。
何回やっても結果が同じ場合の値などをメモリに保存(メモ化)して、初回レンダリング以外に行われる際の再レンダリングでは高価な計算を避けることができます。
このhooksを使う事により、不要な再計算をスキップすることができ、パフォーマンスの向上が期待出来ます。

書き方

useMemo(() => { 重い処理 }, [依存配列]);

依存配列に指定した変数に変更がない場合、重い処理が実行されないようになる。ちなみに、依存配列の価を複数指定した場合はどちらか一方が変更されたら重い処理が実行される。
依存配列が空なら、何にも依存しないことから、初回1回のみ実行。つまり、依存関係が変わらない場合はキャッシュから値(関数の結果の値)を取ってきます。

使用例

import React, { useMemo } from "react";

const App = () => {
  // 再レンダリングされるたびに重い処理が実行される
  const method = superHeavyProcess(a, b);

  //aまたはbが変化したときだけ重い処理が実行される
  const method = useMemo(() => {
    superHeavyProcess(a, b)
  }, [a, b]);

  return (
    ・・・
  )
};

export default App;

このようにuseMemoを使う事により依存配列に指定しているaまたはbが変更されない限り、再レンダリングが行われてもsuperHevyProcess()が実行されなくなります。→ パフォーマンス向上が期待できる。
以下はuseMemoのBad Caseです。

import React, { useMemo } from "react";

const App = () => {
  // Bad Case①
  const hoge = useMemo(() => retern "hoge" , [])

  // Bad Case②
  const method = useMemo(() => {
    superLightProcess(a, b)
  }, [a, b]);

  return (
    ・・・
  )
};

export default App;

useCallback

useCallBackでは関数(コールバック関数)をメモ化するhooksです。
このhooksを使う事により、関数の再生成を防ぐことができパフォーマンス向上が期待できます。

具体的には「親コンポーネントで作られた関数を子コンポーネント」に渡す際に使います。
子コンポーネントに渡さない関数は関数の再生成が行われないのでuseCallbackを使う必要がありません。

書き方

useCallback(() => { callbackFunction }, [依存配列]);

依存配列に指定した変数に変更がない場合、callbackFunctionが実行されないようになる。ちなみに、依存配列の価を複数指定した場合はどちらか一方が変更されたらcallbackFunctionが実行される。依存配列が変わらなければ、前回のcallbackFunctionを再利用する。
依存配列が空なら、コンポーネントがレンダリングされた時に関数が一度生成されますが、以降は同じ関数オブジェクトを再利用するため、再評価されることはありません。

使用例

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

const App = () => {
  const [count, setCount] = useState<number>(0);

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

  return (
    <p>count:{ count }</p>
    <Button onClick={handleClick} />
  )
}

export default App;


import React, { FC } from "react"

type Props = {
  handleClick: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
}
const Button:  FC<Props> = ({ handleClick }) => {
  return (
    <button onClick={handleClick}>カウントアップ</button>
  )
}

export default Button;

このように親コンポーネント(App)で作った関数(handleClick)を子コンポーネント (Button)に渡すときに使います。

React.memo

React.memoはコンポーネントをメモ化したいときに使います。親コンポーネントから渡ってきたpropsが変更されていたら再レンダリングされ、変更がなければ再レンダリングしなくなります。

コンポーネント内で複雑な計算や大量のデータの処理を行っている場合や、プロパティが同一であるコンポーネントの再レンダリングを避けたい場合に使用されます。
また、前章で紹介させてもらったuseCallbackと一緒に使うことが推奨されています。
これらを一緒に使用することで、propsが変更された場合にのみ、新しい関数を生成して再レンダリングをトリガーすることができます。つまり、propsが変更されていない場合には、以前にキャッシュされた関数が使用され、不要な再レンダリングを回避できるということです。なので、必ずしも一緒に使わなければならないわけではありませんが、パフォーマンスの向上を目的とする場合は、一緒に使用することをおすすめします。

書き方

const MemoChild = React.memo(親コンポーネントからpropsを受け取る子コンポーネント)

使用例

import React, { FC } from "react"

type Props = {
  message: string
}
const MemoChild: FC<Props> = React.memo(({ message }) => {
  return <div>{message}</div>;
});
export default MemoChild;

このようにReact.memoを使うと、親コンポーネントから渡ってくるmessageに変化がなければMemoChildは再レンダリングしないとなります。

TIPS

Component definition is missing display name  react/display-name

上記のようなエラーが出る場合があります。
このエラーは、React.memoを使用している場合によく発生します。
React.memoを使用しているときにこのようなエラーが発生する場合、コンポーネントに名前を付けることで解決できます。名前を付ける方法は、以下のように行うことができます

const MemoChild: FC<Props> = React.memo(({ message }) => {
  return <div>{message}</div>;
});

MemoChild.displayName = 'MemoChild';

ここで、displayNameプロパティを使用して、コンポーネントに表示名を指定しています。このようにすることで、React.memoでメモ化されたコンポーネントのエラーが解決されます。
また調べてみると他にも解決策があるそうだったのでこちらの記事を参考にしてみて下さい。
https://qiita.com/tkmd35/items/fb20e1eb36f84dcbc4a1

また、Next.js では、React.memo が無効になることはありませんが、Next.js が提供する最適化技術の一部が、React.memo が提供する効果と重複していることがあります。そのため、「Next.js で React.memo を使用することがあまり意味がない」場合があります。

Next.jsが提供するSSRやSSGなどの機能により、コンポーネントの初回レンダリング時に必要なデータやプロパティがすでにサーバーサイドで取得され、生成されたHTMLに含まれているため、クライアントサイドでの再レンダリング時には不要なデータの再取得が必要なくなります。これにより、コンポーネントの再レンダリングが高速化され、React.memoによるパフォーマンスの向上効果が相対的に小さくなる可能性があります。また、Next.jsのプリフェッチング機能により、事前に必要なデータやリソースを取得し、コンポーネントが必要になったときにすぐに利用できるようになるため、React.memoによる再レンダリングの最適化が不要になる場合もあります。
→元々、Next.jsはページプリレンダリングやコンポーネントレンダリングの最適化を提供しているため。
必ずしもReact.memoを使用する必要がない場合があるということです。ただし、コンポーネントが多くのプロップスを持ち、再レンダリングのオーバーヘッドが高い場合には、React.memoを使用することが依然として効果的であることがあります。

まとめ

これらを使うとパフォーマンスが最適化されますが逆に、「使いすぎたり適切に使わなかったりすればパフォーマンスが下がったりする」ので注意が必要です。
これらを使いこなしてReact初学者から中級者、上級者になりましょう。