React Scan で始めるパフォーマンス最適化
はじめに
React でパフォーマンス最適化を行う際、まずはボトルネックとなっている箇所を特定することから始まる。
本記事ではボトルネックの特定に有用な React Scan を紹介する。
React Scan とは
React Scanは、Reactアプリケーションのパフォーマンスを分析し、最適化するための開発者ツール。具体的には、以下の機能を提供する。
-
レンダリングの可視化:
コンポーネントツリー全体を俯瞰し、各コンポーネントのレンダリング回数を可視化。
どのコンポーネントが、どのような状態変化やプロパティ変更によって再レンダリングされたのかを示す。 -
パフォーマンス問題の検出:
パフォーマンスに悪影響を与える可能性のある無駄なレンダリングを自動的に検出し、表示する。
開発者は、React Scanによって特定されたレンダリングの問題点を把握し、修正することができる。 -
開発効率の向上:
React Scanを使用することで、開発者は React アプリケーションのレンダリング状況を視覚的に把握できる。これにより、パフォーマンスの問題を早期に発見し、効率的に解決することができる。
以下のサイトから気軽に試すことができる。
実際に使ってみる
Setup
以下を参考にしながら実際に Vite で動かしてみる。
まずはプロジェクトを作成し・・・
yarn create vite@latest
index.html に下記を追加する。
<script src="https://unpkg.com/react-scan/dist/auto.global.js"></script>
※今回は CDN から読み込んだが react scan
をインストールする方法もある
Demo
カウンターとテーマを切り替えられるボタンを配置し、Parent コンポーネントと Child コンポーネントの2つを作成した。
Child コンポーネントの中では実行コストの高い関数を呼び出している。
type Theme = {
light: boolean;
dark: boolean;
};
const expensiveFunc = (theme: Theme) => {
const sum = Array.from(
{ length: theme.light ? 2000000 : 1000000 },
(_, i) => i
).reduce((acc, cur) => acc + cur, 0);
return sum;
};
const Child = ({ theme }: { theme: Theme }) => {
const result = React.useMemo(() => expensiveFunc(theme), [theme]);
return (
<div className="child">
<h3>I am a Child.</h3>
<p>result: {result}</p>
<p>theme: {theme.light ? "light" : "dark"}</p>
</div>
);
};
const Parent = () => {
const [count, setCount] = React.useState(0);
const [theme, setTheme] = React.useState("light");
const themeObj = {
light: theme === "light",
dark: theme === "dark",
};
const handleIncrement = () => {
setCount((prev) => prev + 1);
};
const handleToggleTheme = () => {
setTheme(theme === "light" ? "dark" : "light");
};
return (
<div className="parent">
<p>Counter: {count}</p>
<button className="btn" onClick={handleIncrement}>
Increment
</button>
<button className="btn" onClick={handleToggleTheme}>
Toggle Theme
</button>
<Child theme={themeObj} />
</div>
);
};
開発サーバーを立ち上げて画面を操作してみる。
👇レンダリングされるたびにコンポーネント名と回数が表示される
👇特定のコンポーネントに絞ることも可能で、props の変化などの詳細が確認できる
パフォーマンスの問題がありそうな箇所は、警告マークで教えてくれる。
👇また、レンダリングごとに Hisotry を確認できる。
Child コンポーネントで計算コストの高い関数をレンダリングのたびに呼び出しているのでレンダー時間が多くかかっていることが確認できる。
このように React Scan ではパフォーマンスに悪影響を与える可能性のある無駄なレンダリングを自動的に検出し、表示してくれる。
また、ON/OFFの切り替えも簡単に行える。
React Devtools では、ON/OFFの切り替えが面倒だったので便利。
パフォーマンス最適化
メモ化で最適化を行えば不要なレンダリングを防ぐことができる。
現状は React Scan が教えてくれたように、Child コンポーネントの props の値自体は変わっていないのに参照が変わっているのでレンダリングされている。
具体的には、インクリメントして count の値が変わったことによりレンダリングがトリガーされ、themeObj がレンダリングのたびに新規作成され、Child コンポーネントに渡っている。
// レンダリングのたびに作成される
const themeObj = {
light: theme === "light",
dark: theme === "dark",
};
まずは、Child コンポーネントで呼び出している高コスト関数をメモ化する。
React.useMemo
を使用して、theme に変化がない限り複数レンダーを跨いで計算をキャッシュするようにする。
const Child = ({ theme }: { theme: Theme }) => {
// useMemoでラップ
const result = React.useMemo(() => expensiveFunc(theme), [theme]);
return (
<div className="child">
<h3>I am a Child.</h3>
<p>result: {result}</p>
<p>theme: {theme.light ? "light" : "dark"}</p>
</div>
);
};
次に、Parent コンポーネントの themeObj も useMemo でラップする。
const themeObj = React.useMemo(
() => ({
light: theme === "light",
dark: theme === "dark",
}),
[theme]
);
これにより theme の値が変わらない限り、Child コンポーネントで高コストの関数が呼び出されることは無くなった。
👇Increment ボタンを連打しても以前みたいにアラートが出ることは無くなった
しかし、まだ Child コンポーネント自体のレンダリングは行われている。
これはデフォルトで、コンポーネントが再レンダーされると、React はその子要素すべてを再帰的に再レンダーするためである。
これを防ぐには、Child コンポーネントを memo でラップする。
const Child = React.memo(({ theme }: { theme: Theme }) => {
const result = React.useMemo(() => expensiveFunc(theme), [theme]);
return (
<div className="child">
<h3>I am a Child.</h3>
<p>result: {result}</p>
<p>theme: {theme.light ? "light" : "dark"}</p>
</div>
);
});
👇実際に、Child コンポーネントが再レンダリングされていないことを確認できる
まとめ
React DevTools にもレンダリング可視化機能は備わっているが、React Scan は特に原因特定と視覚的な分かりやすさに優れており、より便利だと感じた。
また、今回は CDN 経由で読み込んだが、package manager で react-scan をインストールするとより詳細な検証を行うためのオプションが用意されているみたい。今後はこちらも試してみたい。
Discussion