🐈

React Scanを使ったパフォーマス調整

に公開

React Scanとは

Reactの不要なレンダリングや動作が遅いコンポーネントを検知・可視化するツールです
https://react-scan.com/

環境構築

React Routerを使用して環境構築をして実際に動かしていきます

zsh
npx create-react-router@latest study-react-scan

実行完了したらReact Scanをインストールしていきます

React Scanインストール

zsh
npm i react-scan

インストールできたらReact Scanを使用できるようにします
import.meta.env.DEVを使用して開発環境ではReact Scanを使用できるようにして本番環境の時は使用出来ないように設定してあげます

app/root.tsx
import {
  isRouteErrorResponse,
  Links,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
} from "react-router";

+ import { scan } from "react-scan";
import type { Route } from "./+types/root";
import "./app.css";

+ scan({
+   enabled: import.meta.env.DEV,
+ });

export const links: Route.LinksFunction = () => [
  { rel: "preconnect", href: "https://fonts.googleapis.com" },
  {
    rel: "preconnect",
    href: "https://fonts.gstatic.com",
    crossOrigin: "anonymous",
  },
  {
    rel: "stylesheet",
    href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap",
  },
];
...

ここまで書けたらローカルサーバーを起動してみてください
添付画像のようなものが出ています
ベルマークを押せば詳細な情報が表示されます

実際に動かしてみる

不要レンダリングがあるコード

分かりやすいように最初は不要なレンダリングが発生するようにコードを書いていきます

zsh
mkdir app/components
touch app/components/Button.tsx
app/components/Button.tsx
import { memo } from "react";

export const Button = memo<{
  onClick: () => void;
  children: React.ReactNode;
}>(({ onClick, children }) => {
  return (
    <button
      className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
      onClick={onClick}
    >
      {children}
    </button>
  );
});
zsh
mkdir app/buttonGroup
touch app/buttonGroup/buttonGroup.tsx
app/buttonGroup/buttonGroup.tsx
import { useReducer, useCallback } from "react";
import { Button } from "~/components/button";

const TYPES = {
  ADD: "add",
  SUB: "sub",
  MUL: "mul",
  DIV: "div",
} as const;
type TYPE = (typeof TYPES)[keyof typeof TYPES];

const reducer = (state: number, action: { type: TYPE }) => {
  switch (action.type) {
    case TYPES.ADD:
      return state + 1;
    case TYPES.SUB:
      return state - 1;
    case TYPES.MUL:
      return state * 2;
    case TYPES.DIV:
      return state / 2;
    default:
      return state;
  }
};

export const ButtonGroup = () => {
  const [count, dispatch] = useReducer(reducer, 0);

  return (
    <div className="flex flex-col items-center justify-center h-screen space-y-4">
      <p>Count: {count}</p>
      <Button onClick={() => dispatch({ type: TYPES.ADD })}>Add</Button>
      <Button onClick={() => dispatch({ type: TYPES.SUB })}>Subtract</Button>
      <Button onClick={() => dispatch({ type: TYPES.MUL })}>Multiply</Button>
      <Button onClick={() => dispatch({ type: TYPES.DIV })}>Divide</Button>
    </div>
  );
};
zsh
touch app/routes/button.tsx
app/routes/button.tsx
import type { MetaArgs } from "react-router";
import { ButtonGroup } from "~/buttonGroup/buttonGroup";

export const meta = ({}: MetaArgs) => {
  return [
    { title: "Button" },
    { name: "description", content: "A simple button component" },
  ];
};
export default function Button() {
  return <ButtonGroup />;
}
app/troutes.ts
+ import { type RouteConfig, index, route } from "@react-router/dev/routes";

export default [
  index("routes/home.tsx"),
+   route("button", "routes/button.tsx"),
] satisfies RouteConfig;

ここまで書けたらボタンを押して不要なレンダリングが発生していることを確認してください
一つのボタンを押すと別のボタンをレンダリングされているはずです

不要レンダリングがないコード

React Scanで不要なレンダリングが発生していることを確認した体で修正をしていきます

app/buttonGroup/buttonGroup.tsx
import { useCallback, useReducer } from "react";
import { Button } from "~/components/button";

const TYPES = {
  ADD: "add",
  SUB: "sub",
  MUL: "mul",
  DIV: "div",
} as const;
type TYPE = (typeof TYPES)[keyof typeof TYPES];

const reducer = (state: number, action: { type: TYPE }) => {
  switch (action.type) {
    case TYPES.ADD:
      return state + 1;
    case TYPES.SUB:
      return state - 1;
    case TYPES.MUL:
      return state * 2;
    case TYPES.DIV:
      return state / 2;
    default:
      return state;
  }
};

export const ButtonGroup = () => {
  const [count, dispatch] = useReducer(reducer, 0);

+   const handleAdd = useCallback(() => dispatch({ type: TYPES.ADD }), []);
+   const handleSub = useCallback(() => dispatch({ type: TYPES.SUB }), []);
+   const handleMul = useCallback(() => dispatch({ type: TYPES.MUL }), []);
+   const handleDiv = useCallback(() => dispatch({ type: TYPES.DIV }), []);

  return (
    <div className="flex flex-col items-center justify-center h-screen space-y-4">
      <p>Count: {count}</p>
-       <Button onClick={() => dispatch({ type: TYPES.ADD })}>Add</Button>
-       <Button onClick={() => dispatch({ type: TYPES.SUB })}>Subtract</Button>
-       <Button onClick={() => dispatch({ type: TYPES.MUL })}>Multiply</Button>
-       <Button onClick={() => dispatch({ type: TYPES.DIV })}>Divide</Button>
+       <Button onClick={handleAdd}>Add</Button>
+       <Button onClick={handleSub}>Subtract</Button>
+       <Button onClick={handleMul}>Multiply</Button>
+       <Button onClick={handleDiv}>Divide</Button>
    </div>
  );
};

修正できたら再度確認してみてください
先ほどとは違い一つのボタンを押しても別のボタンは再レンダリングが走らないはずです

まとめ

React Developer Toolsを使用したパフォーマス調査はやっていましたがReact Scanはより直感的に調査しやすいと感じました
また、開発者それぞれでブラウザの拡張機能を入れず開発環境が整えばすぐ使えるのは良いと思いました

Discussion