プログラミング

Reactの再レンダリングの仕組みと最適化を解説【意外と簡単】

Reactの再レンダリングの仕組みと最適化

Reactはアプリが大きくなるほど、再レンダリングのタイミングが複雑になります。
なので、思わぬところでパフォーマンスが下がってしまいます。

つまり、Reactを使っているエンジニアはきちんと、この再レンダリングの最適化を学ぶ必要があるのです。

「Reactを学んだけれど、再レンダリングって何?最適化ってどうやるの?」
という人にはこの記事が役に立つかと思います。

読み終わる頃には、
「あー、再レンダリングの最適化って意外と簡単なのね」
という風になっているかと思います。

本記事の内容
そもそも再レンダリングっていつ起こるの?
再レンダリングを最適化するための3つの方法

それでは、初めていきます。
よりわかりやすいように、本記事は実践形式となっていますので、ぜひ一緒に進めていきましょう。

0からReactエンジニアになるための手順は、全てここで解説しているので合わせてどうぞ。

Reactロードマップ
【完全無料ロードマップ】Reactエンジニアへなるための方法まとめ【現役Reactエンジニアが解説】Reactを学びたいと思ってますか?Reactエンジニアから勉強方法を学ぶのが一番効率的です。なので今回は、現役のReactエンジニアがReactの学習ロードマップを無料で公開します。Reactは今とても需要が高いスキルなので、今すぐ学びましょう。...

そもそも再レンダリングはいつ起こるのか?

まず、Reactにおいて再レンダリングがいつ起こるのか?
ということですが、それは3つあります。

  1. stateが更新された時
  2. propsが更新された時
  3. 親コンポーネントが再レンダリングされた時

それぞれ詳しく説明していきます。

1、stateが更新された時

const CountUp = () => {
  const [count, setCount] = useState(0);
  return(
    <>
      <h2>count up</h2>
      <button onClick={() => setCount(count + 1)}>count up!</button>
      <p>{count}</p>
    </>
  )
}

上記のコードではボタンが押された時に、「count」という「state」が更新されるようになってます。

このように、「state」が更新される時に再レンダリングが起こります。

これが、「state」が更新されたときの例です。

2、propsが更新された時

次に、「props」が更新された時です。

こちらは、単体で例を出すのが難しいので、後で詳しく説明します。

とりあえず今は、「props」が更新された時も再レンダリングが起こる。

ということを覚えておけばOKです。

3、親コンポーネントが再レンダリングされた時

最後は、親コンポーネントが再レンダリングされた時です。
例えば、1のコードに子コンポーネントを追加します。

const Child = () => {
  console.log("child is called");
  return(
    <h3>child</h3>
  );
}

export const CountUp = () => {
  const [count, setCount] = useState(0);
  return(
    <>
      <h2>count up</h2>
      <button onClick={() => setCount(count + 1)}>count up!</button>
      <p>{count}</p>
      <Child />
    </>
  )
}

親コンポーネントの「state」を変更するような処理を行った場合は、親コンポーネントだけでなく、子コンポーネントも再レンダリングされます。

ボタンを押す度に「child is called」とログに表示されているかと思います。

これが毎回レンダリングが走っている証明になります。

再レンダリングを最適化するための3つの方法

さて、再レンダリングが起こる3つのタイミングを見てきました。

では実際に、
「どうやればこの再レンダリングを最適化できるのか?」
ということを実践を通して学んでいきましょう。

再レンダリングを最適化するための方法は主に3つあります。

  1. memo
  2. useCallback
  3. useMemo

1つずつ解説していきます。

0、環境構築

まずは、実践形式で学んでいくために環境構築をしましょう。

何も難しいことはなくcreate-react-appをすればOkです。

後は、「App.js」の一番外の「divタグ」以外を消します。

そして、src配下に「components/memo」というディレクトリを作りましょう。

そこに、「CountUp.jsx」というファイルを作り関数コンポーネントの雛形だけを書きエクスポートして、「App.js」側で表示させれば準備完了です。

1、memo

まず、こちらの機能を説明するために簡単なカウントアップの機能を実装していきます。
そして、その子のChildコンポーネントを書きます。

コードは以下の通り、

import {useState, memo} from "react";

const Child = () => {
  console.log("child is called");
  return(
    <h3>child</h3>
  );
}

export const CountUp = () => {
  const [count, setCount] = useState(0);
  return(
    <>
      <h2>count up</h2>
      <button onClick={() => setCount(count + 1)}>count up!</button>
      <p>{count}</p>
      <Child />
    </>
  )
}

この状態だと、カウントアップされる度にログに「child is called」と表示されるかと思います。

これは、CountUpコンポーネントの「state」である「count」が更新されレンダリングが起こり、その子であるChildコンポーネントも再レンダリングが起こるためです。

しかし、Childコンポーネントの中身は何1つ変わっていないので、実際再レンダリングは不要です。

そこで、役に立つのが「memo」という機能です。

使い方は簡単で、コンポーネントをを「memo」という文字で囲むだけでOKです。

これをすることで、「memo」で囲まれたコンポーネントは、例え親コンポーネントが更新されてもpropsが変更されない限りは再レンダリングされなくなります。

つまり、不要な再レンダリングを起こさなくなります。

memo化したchildコンポーネントは以下の通り。

import {useState, memo} from "react";

const Child = memo(() => {
  console.log("child is called");
  return(
    <h3>child</h3>
  );
});

ちなみに、memoを使うためにはきちんとインポートしておく必要がありますので忘れないように。

2、useCallback

次も、まずは解説の前に必要な機能を実装していきます。

「childCount」という「state」を定義します。

次に、「onClickChildCount」という「childCount」をカウントアップしていく関数を定義します。

そして、それをChildコンポーネントに渡します。

Childコンポーネント内でそれを使って、「childCount」をカウントアップしていくボタンを作成します。

これで、準備完了です。

import {useState, memo, useCallback} from "react";

const Child = memo((props) => {
  console.log("child is called");
  return(
    <div>
      <h3>child</h3>
      <button onClick={props.onClick}>child count up!</button>
    </div>
  );
});

export const CountUp = () => {
  const [count, setCount] = useState(0);
  const [childCount, setChildCount] = useState(0);

  const onClickChildCount = () => {
    setChildCount(childCount + 1);
  };

  return(
    <>
      <h2>count up</h2>
      <button onClick={() => setCount(count + 1)}>count up!</button>
      <p>{count}</p>
      <Child onClick={onClickChildCount}/>
      <p>{childCount}</p>
    </>
  )
}

では、一番上のカウントアップボタンを押してください。

ログを見て何か気づきましたか?

そうです。

せっかく先ほどmemo化したChildコンポーネントがまた再レンダリングされるようになってしまっているのです。

なぜかと言うと、今回は「props」としてアロー関数で作った関数を渡しているのですが、このアロー関数で作った関数は毎回新しい関数が生成されていると判断されるからです。

なので、「props」が更新されている判断がされ、再レンダリングが起きているのです。

そして、それを解決するのが「useCallback」です。

これは、変数を引数として渡し、その変数の変更がない場合は再生成を行わないようにしてくれます。

import {useState, memo, useCallback} from "react";

const Child = memo((props) => {
  console.log("child is called");
  return(
    <div>
      <h3>child</h3>
      <button onClick={props.onClick}>child count up!</button>
    </div>
  );
});

export const CountUp = () => {
  const [count, setCount] = useState(0);
  const [childCount, setChildCount] = useState(0);

  const onClickChildCount = useCallback(() => {
    setChildCount(childCount + 1);
  },[childCount]);

  return(
    <>
      <h2>count up</h2>
      <button onClick={() => setCount(count + 1)}>count up!</button>
      <p>{count}</p>
      <Child onClick={onClickChildCount}/>
      <p>{childCount}</p>
    </>
  )
}

これで、親コンポーネントが再レンダリングされても、再レンダリングが起きなくなっているかと思います。

3、useMemo

最後に紹介するのが、「useMemo」になります。

これは、変数をmemo化するための機能です。

まず、説明のために追加の機能を実装していきます。

「count」の数を2倍したものを「dubledCount」という変数に入れて、それを表示させるようにします。

コードは以下の通り。

import {useState, memo, useCallback, useMemo} from "react";

const Child = memo((props) => {
  console.log("child is called");
  return(
    <div>
      <h3>child</h3>
      <button onClick={props.onClick}>child count up!</button>
    </div>
  );
});

export const CountUp = () => {
  const [count, setCount] = useState(0);
  const [childCount, setChildCount] = useState(0);

  const onClickChildCount = useCallback(() => {
    setChildCount(childCount + 1);
  },[childCount]);

  const dubleCount = (count) => {
    console.log("call dublecount")
    return(count * 2);
  }

  const dubledCount = () => dubleCount(count);

  return(
    <>
      <h2>count up</h2>
      <button onClick={() => setCount(count + 1)}>count up!</button>
      <p>{count}</p>
      <p>{dubledCount}</p>
      <Child onClick={onClickChildCount}/>
      <p>{childCount}</p>
    </>
  )
}

このままでは、「count」が変更されていない時でも毎回2倍の計算がされて無駄になってしまいます。

試しに、「childCount」の方のカウントアップボタンを押した時も、ログに「call dublecount」と表示されてしまっているかと思います。

今回の例ならまだ良いのですが、重い処理とかになると無駄にパフォーマンスが下がってしまいます。

なので、memo化して「count」が変更されていない時は2倍の計算がされないようにします。

memo化したコードは以下の通りです。

const Child = memo((props) => {
  console.log("child is called");
  return(
    <div>
      <h3>child</h3>
      <button onClick={props.onClick}>child count up!</button>
    </div>
  );
});

export const CountUp = () => {
  const [count, setCount] = useState(0);
  const [childCount, setChildCount] = useState(0);

  const onClickChildCount = useCallback(() => {
    setChildCount(childCount + 1);
  },[childCount]);

  const dubleCount = (count) => {
    console.log("call dublecount")
    return(count * 2);
  }

  const dubledCount = useMemo(() => dubleCount(count), [count]);

  return(
    <>
      <h2>count up</h2>
      <button onClick={() => setCount(count + 1)}>count up!</button>
      <p>{count}</p>
      <p>{dubledCount}</p>
      <Child onClick={onClickChildCount}/>
      <p>{childCount}</p>
    </>
  )
}

これで毎回不要なレンダリングが起きなくなります。

Reactの再レンダリングの仕組みと最適化を解説 まとめ

本記事では、Reactの再レンダリングのタイミングとそれを最適化するための方法を学びました。

まとめます。

再レンダリングのタイミング

  • stateが更新された時
  • propsが更新された時
  • 親コンポーネントが再レンダリングされた時

最適化するための方法
1、memo
Propsが更新されてない時は、親コンポーネントが再レンダリングされても、memo化したコンポーネントは再レンダリングされなくなる
2、useCallback
Propsにアロー関数を渡している時は、毎回新しい関数を再生成されているために起こる不要な再レンダリングを無くす
3、useMemo
変数をmemo化して、レンダリングの最適化をする

意外とここをわかってない人とかいるので、知ってるだけでかなり重宝される存在になれるかと思います。

今回学んだ知識は忘れてもすぐに見直せるように、githubなどに上げておくことをオススメします。

僕のブログでも他にプログラミングについて解説している記事がありますので、興味のある方はぜひ見てください。

Reactエンジニアになりたい人はこちらからどうぞ

Reactロードマップ
【完全無料ロードマップ】Reactエンジニアへなるための方法まとめ【現役Reactエンジニアが解説】Reactを学びたいと思ってますか?Reactエンジニアから勉強方法を学ぶのが一番効率的です。なので今回は、現役のReactエンジニアがReactの学習ロードマップを無料で公開します。Reactは今とても需要が高いスキルなので、今すぐ学びましょう。...

また、今回はコードはこちらにも載ってますので、参考にしてみてください。
https://github.com/hinoharashinya/react-commentary/tree/master/src/components/memo

おわり

ABOUT ME
ひのしん
【執筆者情報】ひのしんです。「正しい努力をすれば誰でも成果は出せる」 をモットーに発信しています。元ニートの現フリーランスエンジニアです。 プログラミング歴は4年ほどで、TOEIC885点を取得。科学と実体験を元にした効率的で正しいスキルアップ(プログラミング・英語・ブログ)の方法を発信してます。
\\こちらの記事が大好評です//