こんにちは、なかにしです。
今回は「パフォーマンスチューニング」についてまとめていきます!
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!!