React Scanをさわってみた
この記事は Applibot Advent Calendar 2024 7日目の記事です。前回の記事はコチラです。
この記事は?
普段Reactを用いてWebアプリケーションを開発している上で、Chrome DevToolsやReact Developer Toolsにとてもお世話になっているのですが、さわりはじめの頃は表示されている情報がどのような意味を持った情報かわからず、ツールに慣れるまでそこそこ時間を要していました。
もう少し使いやすく、直感的に理解できるツールは無いものかと悩んでいた際に運良く React Scan に出会ったため今回はこれをさわっていこうかと思います。
React Scanとは
React Scan
は、Reactアプリ全体のレンダリング状況を可視化することに特化したツールです。同ツールのREADMEにある「Why React Scan?」セクションでも強調されているとおり、Reactのパフォーマンス測定で一般的なChrome DevToolsやReact Developer Toolsは豊富な情報源であり、かつ強力な開発ツールである一方、以下のような課題が残っています。
-
断片的な情報への依存
Profilerは「いつ何がどれくらいの時間で描画されたか」を示しますが、「なぜ、そのコンポーネントが再レンダリングされたのか?」を明確に示すには開発者側の推測が必要でした。 -
再レンダリング原因の不透明性
状態管理やプロパティの変更がどのように伝播してレンダリングを引き起こしているかを把握するには、手間と経験、もしくは開発しているアプリへの深い理解が必要でした。
ここでReact Scan
は、より直感的にこのレンダリングを把握することができます。
React Scan
は、Reactアプリのコンポーネントツリーを俯瞰し、どのコンポーネントが何回再レンダリングされたかを可視化します。また、それらの再レンダリングがどの状態変化やプロパティ変更に起因するかを分かりやすく示すことで、開発者は 「なぜ」 を解明しやすくなります。
react-scan
の主な特徴
-
コードの変更が不要:
既存アプリのソースコードへの組み込みは不要です。CLIで検証したい対象アプリのURLを指定すればすぐに確認することができます。 -
直感的なUI
レンダリングが発生したコンポーネントの強調表示、再レンダリングの回数やかかった時間の表示をコンポーネント単位で表示してくれます。 -
最適化の指針提供:
再レンダリングがどのステートやプロパティ変更に起因するかを示唆するため、メモ化やコンポーネント設計の改善点を検討しやすくなります。
使い方
インストールと基本的なセットアップ
主な特徴で挙げた通り、最速で確認したいのであればCLIを使用することですぐに React Scan
を使い始めることができます。
# local環境で起動しているアプリを確認する場合
npx react-scan@0.0.36 http://localhost:3000
# Web上で動作しているアプリを確認する場合
npx react-scan@0.0.36 https://react.dev
コードを変更せずに確認することが魅力ですが、コードベースへアクセスできるのであれば下記のscriptをコードへ組み込むことにより React Scan
を使用することができます。
<script src="https://unpkg.com/react-scan/dist/auto.global.js"></script>
例えば Next.js の App Router の場合は下記のようにルートの layout.tsx へ組み込む例がREADMEに記載されています。
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<head>
<script src="https://unpkg.com/react-scan/dist/auto.global.js" async />
</head>
<body>{children}</body>
</html>
)
}
React Scan の基本的な使い方
ChatGPTに出力してもらった再レンダリングマシマシなコードをベースに React Scan
の動作を確認していきたいと思います。
レンダリングマシマシコード
'use client'
import List from '@/components/list'
export default function Home() {
return (
<main>
<div className="min-h-screen bg-gray-100 flex justify-items-start justify-start p-8">
<List />
</div>
</main>
)
}
import { FC, useState } from 'react'
import ListItem from './list-item'
import Input from './input'
import Button from './button'
const List: FC = () => {
const [inputValue, setInputValue] = useState('')
const [items, setItems] = useState<string[]>([])
const handleAddItem = () => {
if (inputValue.trim() === '') return
setItems([...items, inputValue.trim()])
setInputValue('')
}
const handleRemoveItem = (index: number) => {
const newItems = [...items]
newItems.splice(index, 1)
setItems(newItems)
}
return (
<div className="flex flex-col gap-4 w-full max-w-md bg-white rounded-md shadow p-6 h-fit">
<div className="flex space-x-2 mb-4">
<Input inputValue={inputValue} setInputValue={setInputValue} />
<Button handleAddItem={handleAddItem} />
</div>
<ul className="space-y-2">
{items.map((item, index) => (
<ListItem
key={index}
item={item}
index={index}
handleRemoveItem={handleRemoveItem}
/>
))}
</ul>
</div>
)
}
export default List
import { FC } from 'react';
type Props = {
item: string
index: number;
handleRemoveItem: (index: number) => void;
}
const ListItem: FC<Props> = ({
item,
index,
handleRemoveItem,
}) => {
return (
<li key={index} className="flex justify-between items-center border-b border-gray-200 py-2">
<span className="text-gray-700">{item}</span>
<button
onClick={() => handleRemoveItem(index)}
className="text-red-500 hover:text-red-700 focus:outline-none"
>
削除
</button>
</li>
)
}
export default ListItem
import { ComponentProps, FC } from 'react';
type Props = {
inputValue: ComponentProps<'input'>['value'];
setInputValue: (value: string) => void;
}
const Input: FC<Props> = ({
inputValue,
setInputValue,
}) => {
return (
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
className="flex-grow border border-gray-300 p-2 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 text-black"
placeholder="文字列または数値を入力"
/>
)
}
export default Input
import { ComponentProps, FC } from 'react'
type Props = {
handleAddItem: ComponentProps<'button'>['onClick'];
}
const Button: FC<Props> = ({ handleAddItem }) => {
return (
<button
type="button"
onClick={handleAddItem}
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 focus:outline-none"
>
追加
</button>
)
}
export default Button
まず、npxもしくはscriptを埋め込んだ状態で確認すると下記の画像のような画面になります。
このときに右上に見えているUIが React Scan
の操作パネルとなります。
このままテキストインプットに文字列を入力すると、stateの更新に影響を受けたコンポーネントの再レンダリングが可視化されます。同時にレンダリングされた回数や、かかった時間なども合わせて表示されます。
また、Chrome DevToolsのように任意の要素を選択し、選択された要素が持つpropsやcontextなどを表示する機能もあります。
さらに、最適化余地のあるpropsに対しては画像のように「⚠️」を付与して教えてくれます。
基本的な使い方としては React Developer Tools における Components や Profiler をより直感的に利用できるよう可視化されたツールという理解ができそうです。
これと合わせて React Developer Tools のレンダリングが発生した要素のハイライト表示や、Profiler の Why did this render? を利用することにより、更に強力な検証を行うことが期待できます。
使い方応用編: ブラウザ拡張機能
本記事執筆時点で React Scan
のブラウザ拡張機能は、Chrome ウェブストア、Firefox アドオン、Brave ブラウザからの承認待ちです。
もし、先んじて各ブラウザにおいて拡張機能を利用したい場合はここを参考にして手動で拡張機能を登録し利用することができます。
使い方応用編: プログラマブルなデバッキング
React Scan
はCLIや配信されているjsをscriptとしての組み込み以外にも、より詳細な検証を行うためにAPIを用意しています。アプリに React Scan
をインストールし検証したいコンポーネントへコードベースでAPIを埋め込み確認することができます。
npm install react-scan
pnpm add react-scan
Options
export interface Options {
/**
* Enable/disable scanning
*
* Please use the recommended way:
* enabled: process.env.NODE_ENV === 'development',
*
* @default true
*/
enabled?: boolean;
/**
* Include children of a component applied with withScan
*
* @default true
*/
includeChildren?: boolean;
/**
* Enable/disable geiger sound
*
* @default true
*/
playSound?: boolean;
/**
* Log renders to the console
*
* @default false
*/
log?: boolean;
/**
* Show toolbar bar
*
* @default true
*/
showToolbar?: boolean;
/**
* Render count threshold, only show
* when a component renders more than this
*
* @default 0
*/
renderCountThreshold?: number;
/**
* Clear aggregated fibers after this time in milliseconds
*
* @default 5000
*/
resetCountTimeout?: number;
/**
* Maximum number of renders for red indicator
*
* @default 20
*/
maxRenders?: number;
/**
* Report data to getReport()
*
* @default false
*/
report?: boolean;
/**
* Always show labels
*
* @default false
*/
alwaysShowLabels?: boolean;
/**
* Animation speed
*
* @default "fast"
*/
animationSpeed?: 'slow' | 'fast' | 'off';
onCommitStart?: () => void;
onRender?: (fiber: Fiber, render: Render) => void;
onCommitFinish?: () => void;
onPaintStart?: (outlines: PendingOutline[]) => void;
onPaintFinish?: (outlines: PendingOutline[]) => void;
}
各オプションを設定し React Developer Tools の機能、例えばレンダリングが発生したコンポーネントのハイライト表示を組み合わせると、より強力な検証を行うことができるかもしれません。(下記は alwaysShowLabels
をtrueに設定しハイライト表示とコンポーネント名の表示を組み合わせてみた例)
まとめ
React Scan
は現状でも強力な可視化ツールです。React Scan
を利用すればパフォーマンスの問題を引き起こしているレンダリングを自動的に検出し強調表示してくれるため、問題の箇所をいちはやく特定するのに役立ちます。また、React Developer Tools のように従来はChromeブラウザでしか確認することができなかったレンダリング問題を他ブラウザでも確認できるようになるのも魅力的なポイントです。
今回さわれていない部分も数多くあるため、課題解決のためのヒントが数多く転がっているのかと思うとワクワクが止まらないです!
React Scan
はまだまだ開発中でありめざましい早さで機能の追加や改善が行われています。今後ますます強力な開発ツールへと成長してくことが期待できます。
おまけ
個人的に、作者の方のX(旧twitter)での火力強めな投稿がおもしろかったです。
Discussion