技術系

[React]パフォーマンスチューニング

技術系

こんにちは、なかにしです。

今回は「パフォーマンスチューニング」についてまとめていきます!

useStateやuseEffectにも慣れてきて、
そろそろuseMemoとかuseCallback使いたいな~と思う年頃になりました。

いってみよう!

パフォーマンスチューニングについて

一言で言うと、パフォーマンス改善です。
名前がかっこいいですよね。

今回行うチューニングは以下を使用します。

①コンポーネントのMemo化
②関数のMemo化
③変数のMemo化

①はReact.Memo、②はuseCallback、③はuseMemoを使用します。

①に関しては前回の記事で紹介しているので、
今回は②③を実施します!

関数をMemo化

useCallbackを使用します!
使い時としては、「子に関数を渡すとき」です!

以下の例をご覧ください。

普通のボタンとButtonコンポーネントで作成したボタンを配置し、
カウントアップさせる処理を書いています。

import "./App.css";
import React, { useState } from "react";

const Button: any = React.memo(({ onClick }: any) => {
  console.log("ボタンコンポーネントを呼び出し");
  return <button onClick={onClick}>子ボタン</button>;
});

const App = () => {
  const [count, setCount] = useState(0);
  console.log("Appコンポーネントを呼び出し");

  const onClick = () => {
    console.log("onClick関数を呼び出し");
    setCount((prev) => prev + 1);
  };

  return (
    <>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>カウントアップ</button>
      <Button onClick={onClick} />
    </>
  );
};

export default App;

ボタンコンポーネントはMemo化し、
普通のボタン押下時の再レンダリングを防いでいます。

▽これで問題なしと思いきや…

普通のボタン押下時も「ボタンコンポーネント」が再描画されています。

これはReactというかJSの考え方なのですが、
「再描画される前のonClick関数」と「再描画された後のonClick関数」は
別のものという認識になります。

中身が一緒でも、
リロードしてもう一回作ったら違うものとして扱うんですね。

見た目が一緒な別のonClickをボタンコンポーネントに渡しているので、
ボタンコンポーネントが再描画されるという仕組みです。

よしなにやってくれよJSさん…

これを解決する為、useCallbackを使用します。
中身が一緒だったら同じものとして扱ってね!という宣言ですね。

対象をuseCallbackで囲みます。

import "./App.css";
import React, { useCallback, useState } from "react";

const Button: any = React.memo(({ onClick }: any) => {
  console.log("ボタンコンポーネントを呼び出し");
  return <button onClick={onClick}>子ボタン</button>;
});

const App = () => {
  const [count, setCount] = useState(0);
  console.log("Appコンポーネントを呼び出し");

  const onClick = useCallback(() => {
    setCount((prev) => prev + 1);
  }, []);

  return (
    <>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>カウントアップ</button>
      <Button onClick={onClick} />
    </>
  );
};

export default App;

▽動作確認

onClickの処理の中身は不変なので、再描画されていません。
これで余計に呼び出さなくて済むようになりました!やったね!

変数をMemo化

useMemoは変数に対して、余計なレンダリングを防ぎます。
関係ないときは呼び出さないで!って感じです。

まずは以下をご覧ください。

import "./App.css";
import { useState } from "react";

function App() {
  const [number, setNumber] = useState(1);
  const [number2, setNumber2] = useState(1);

  const countUp2 = () => {
    console.log("カウントアップ2を計算するよ!");
    setNumber2((prevNumber) => prevNumber + 1);
  };

  const countUp = () => {
    console.log("カウントアップ1を計算するよ!");
    setNumber((prevNumber) => prevNumber + 1);
  };

  const double = (number2: number) => {
    console.log("2倍の値を計算するよ!");
    return number2 * 2;
  };

  const doubleCount = double(number2);

  return (
    <div className="App">
      <p>カウント1は{number}</p>
      <p>カウント2は{number2}</p>
      <p>カウント2の2倍の値は{doubleCount}</p>

      <button onClick={() => countUp()}>カウントアップ1</button>
      <button onClick={() => countUp2()}>カウントアップ2</button>
    </div>
  );
}

export default App;

▽動作確認

number1, number2, doubleCount という3つの変数を用意しました。

ボタンを押すと、number1, number2がそれぞれカウントアップします。
doubleCountはnumber2の値を2倍した数が入ります。

コンソールを見ると、
現状はどちらのボタンを押してもdoubleCountが再計算されています。

doubleCountは「number2の値を2倍」したものなので、
「number1」がなんの数字になろうが関係ありません。

しかし、useStateが変更されて再描画する関係上、毎回呼び出されてしまいます。

今回のような軽い処理ならばいいのですが、
doubleがめっちゃ重い処理だったらどうでしょう。

▽試しに重くしてみます。

  const double = (number2: number) => {
    console.log("2倍の値を計算するよ!");
    let i = 0;
    while (i < 1000000000) i++;
    return number2 * 2;
  };

doubleをめっちゃ重くしました。
doubleが呼ばれるたびに10億回 i を足し算する処理を挟みます。

▽動作確認

ボタンを押してから表示までが明らかに遅くなってますよね。
計測してみると、両方ともボタンを押すたびに「230 ~ 240ミリ秒」かかっています。

これをメモ化します。
メモ化したい変数をuseMemoで囲み、第2引数に監視対象を指定します。

import "./App.css";
import { useState, useMemo } from "react";

function App() {
  const [number, setNumber] = useState(1);
  const [number2, setNumber2] = useState(1);

  const countUp2 = () => {
    console.log("カウントアップ2を計算するよ!");
    setNumber2((prevNumber) => prevNumber + 1);
  };

  const countUp = () => {
    console.log("カウントアップ1を計算するよ!");
    setNumber((prevNumber) => prevNumber + 1);
  };

  const double = (number2: number) => {
    console.log("2倍の値を計算するよ!");
    let i = 0;
    while (i < 1000000000) i++;
    return number2 * 2;
  };

  const doubleCount = useMemo(() => double(number2), [number2]);

  return (
    <div className="App">
      <p>カウント1は{number}</p>
      <p>カウント2は{number2}</p>
      <p>カウント2の2倍の値は{doubleCount}</p>

      <button onClick={() => countUp()}>カウントアップ1</button>
      <button onClick={() => countUp2()}>カウントアップ2</button>
    </div>
  );
}

export default App;

doubleCount変数をMemo化し、監視対象にnumber2を指定しました。
これでnumber2が変化しなければdoubleCountは呼び出されません。

▽動作確認

コンソールを見ると、number1が変更されたときは
doubleCountが呼び出されていないことが分かるかと思います。

計測してみると、number2が変化するボタンは変わらず「230 ~ 240ミリ秒」でしたが、
number1が変化するんボタンは「0.8 ~ 0.9ミリ秒」でした。

かなりの改善が見られましたね!

おわりに

注意していただきたいのが、useMemo, useCallbackともに万能ではないということです。

useMemo,useCallbackにもコストがかかるので、
「とりあえずMemo化」は逆効果の場合があることを念頭に置いておいてください。

とはいえ便利なことには変わりないので、
速度を計測しながら積極的に使っていきましょう!

今回はここまで!
Enjoy Hacking!!

タイトルとURLをコピーしました