1. ホーム
  2. React

【React】Profiler の使い方|コンポーネントのレンダリング性能を計測する

Share

React アプリの表示が重いと感じたとき、「どのコンポーネントのレンダリングに時間がかかっているのか」を数値で知りたくなります。React には、そのためのビルトイン API として <Profiler> コンポーネントが用意されています。この記事では、<Profiler> で対象ツリーを囲む基本の書き方から、onRender コールバックに渡される各引数の意味、複数の Profiler をネスト・並列で使う例、本番ビルドでは既定で無効になる点、そして React DevTools の Profiler タブとの使い分けまでを、動く TSX コードとともに解説します。React 公式仕様に沿って正確にまとめます。

Profiler コンポーネントで何ができるのか

<Profiler> は、React ツリーの一部分のレンダリング性能をプログラム的に計測するためのコンポーネントです。計測したい範囲を <Profiler> で囲んでおくと、その範囲内でレンダー(コミット)が起きるたびに、指定したコールバック関数へ「今回のレンダーにどれだけ時間がかかったか」といった情報が渡ってきます。

後述する React DevTools の Profiler タブが「開発者が手動で操作して眺める」ツールなのに対し、<Profiler> コンポーネントは計測結果をコードで受け取れるのが特徴です。そのため、計測値をログに出したり、独自の集計・分析に回したりといった使い方ができます。

基本の使い方:id と onRender で囲む

<Profiler> には、必須の props が2つあります。計測範囲を識別するための文字列 id と、レンダーがコミットされるたびに呼ばれる onRender コールバックです。計測したいコンポーネントツリーを、この2つを指定した <Profiler> で囲みます。

App.tsx
import { Profiler } from 'react';
import { Sidebar } from './Sidebar';

export function App() {
  return (
    <Profiler
      id="Sidebar"
      onRender={(id, phase, actualDuration) => {
        // レンダーがコミットされるたびに呼ばれる
        console.log(id, phase, actualDuration);
      }}
    >
      <Sidebar />
    </Profiler>
  );
}

id は、複数の計測範囲を区別するためのラベルです。あとで説明するように複数の <Profiler> を使うと、コールバックの第1引数にこの id が渡ってくるため、どの範囲の計測結果なのかを見分けられます。onRender は、囲んだツリー内でレンダーがコミットされたとき(=画面への反映が確定したとき)に呼ばれる関数です。次の節で、この関数が受け取る引数を詳しく見ていきます。

onRender コールバックに渡される引数

onRender は、React から次の6つの引数を受け取ります。それぞれの意味は以下のとおりです。

引数意味
idstring計測結果を出した <Profiler>id prop。どの範囲の計測かを見分けるのに使う。
phase“mount” | “update” | “nested-update”今回のレンダーの種類。初回マウントか、再レンダー(更新)か、更新中に発生したネストされた更新かを表す。
actualDurationnumber今回のコミットで <Profiler> とその子孫のレンダーに実際にかかったミリ秒。メモ化(memo / useMemo)が効いていれば、この値は小さくなる。
baseDurationnumberメモ化を一切行わずにツリー全体を再レンダーした場合にかかると推定されるミリ秒。最悪ケースのコスト目安。
startTimenumber今回のレンダーを React が開始した時点のタイムスタンプ(ミリ秒)。
commitTimenumber今回のレンダーを React がコミットした時点のタイムスタンプ(ミリ秒)。同一コミット内の複数の Profiler で共通の値になる。

性能改善の観点で特に重要なのが actualDurationbaseDuration の関係です。baseDuration は「何も最適化しなければこれくらいかかる」という基準値で、actualDuration は「実際にかかった時間」です。メモ化がうまく効いているコンポーネントでは、再レンダー時に actualDurationbaseDuration より小さくなります。逆に両者がほぼ同じなら、その範囲では最適化の余地があると判断できます。

phase"nested-update" は少し分かりにくい値です。これは、あるコミットの処理中(たとえば useLayoutEffect の中など)に setState が呼ばれ、その場で追加のレンダーが発生したケースを表します。通常の更新は "update"、初回表示は "mount" になります。

実際にこれらの引数を使って計測ログを整形すると、次のようになります。

onRenderCallback.tsx
import type { ProfilerOnRenderCallback } from 'react';

// onRender に渡す関数。型は ProfilerOnRenderCallback を使うと安全
const onRender: ProfilerOnRenderCallback = (
  id,
  phase,
  actualDuration,
  baseDuration,
  startTime,
  commitTime,
) => {
  console.log(`[${id}] ${phase}`);
  console.log(`  実測: ${actualDuration.toFixed(2)}ms`);
  console.log(`  基準: ${baseDuration.toFixed(2)}ms`);
  console.log(`  開始: ${startTime.toFixed(2)} / コミット: ${commitTime.toFixed(2)}`);
};

export default onRender;

TypeScript では、React が提供する ProfilerOnRenderCallback 型を使うと、6つの引数の型を自分で書かずに済みます。

複数の Profiler をネスト・並列で使う

<Profiler> は、アプリ内で何個でも使えます。別々の範囲を並列に計測したいときは、それぞれ異なる id を付けた <Profiler> で個別に囲みます。

ParallelProfilers.tsx
import { Profiler } from 'react';
import onRender from './onRenderCallback';
import { Header } from './Header';
import { Content } from './Content';

export function ParallelProfilers() {
  return (
    <>
      {/* Header と Content を別々の id で計測する */}
      <Profiler id="Header" onRender={onRender}>
        <Header />
      </Profiler>
      <Profiler id="Content" onRender={onRender}>
        <Content />
      </Profiler>
    </>
  );
}

コールバックは共通の onRender を使い回していますが、第1引数の id"Header""Content" かで、どちらの計測結果なのかを判別できます。

また、<Profiler>ネスト(入れ子)することもできます。大きな範囲を外側で計測しつつ、その中の特定の重い部分をさらに内側で細かく計測する、といった使い方です。

NestedProfilers.tsx
import { Profiler } from 'react';
import onRender from './onRenderCallback';
import { Panel } from './Panel';
import { HeavyChart } from './HeavyChart';

export function NestedProfilers() {
  return (
    <Profiler id="Panel" onRender={onRender}>
      <Panel>
        {/* Panel 全体の中の、特に重い Chart だけを個別に計測 */}
        <Profiler id="HeavyChart" onRender={onRender}>
          <HeavyChart />
        </Profiler>
      </Panel>
    </Profiler>
  );
}

ネストした場合、外側の "Panel"actualDuration には内側の "HeavyChart" のレンダー時間も含まれます。「パネル全体では何ミリ秒かかり、そのうちチャート部分が何ミリ秒か」を切り分けて把握できるわけです。

本番ビルドでは既定で無効になる

<Profiler> を使ううえで必ず知っておきたいのが、計測には追加のオーバーヘッドがあるため、本番ビルドでは既定で無効になっているという点です。開発ビルドでは普通に onRender が呼ばれますが、通常の本番ビルドでは <Profiler> は素通しになり、コールバックは呼ばれません。

本番相当の環境で計測したい場合は、React が用意しているプロファイリング用のビルド(profiling build)を明示的に使う必要があります。React 自身をプロファイリング対応版のバンドルに差し替えることで、本番モードのままでも計測が有効になります。実運用のアプリに常時入れておくものではなく、計測したいときだけ切り替える性質のものだと考えてください。

この仕様のため、onRender の中で通常のアプリのロジックを動かすような書き方は避けるべきです。あくまで計測・記録のためのコードにとどめ、本番で無効化されても機能に影響が出ないようにしておくのが安全です。

React DevTools の Profiler タブとの違い・使い分け

React には、コンポーネントの性能を計測する手段として、この <Profiler> コンポーネントとは別に、ブラウザ拡張機能 React DevTools の Profiler タブもあります。両者は目的が近いものの、使いどころが異なります。

<Profiler> コンポーネントDevTools の Profiler タブ
計測結果の受け取り方onRender でコードとして受け取る拡張機能の画面で視覚的に確認する
計測範囲囲んだツリーだけをピンポイントで記録中のアプリ全体を対象に
向いている用途特定範囲の値を継続的に記録・集計する手動操作しながらボトルネックを探る
準備コードに <Profiler> を書き足す拡張機能を入れて記録ボタンを押すだけ

ふだんの調査では、まず DevTools の Profiler タブで記録を取り、フレームチャートを眺めながら「どのコンポーネントが重いか」の当たりを付けるのが手軽です。コードを書き換えずに、操作しながら全体を俯瞰できるのが強みです。

一方、<Profiler> コンポーネントは、当たりを付けた特定範囲の actualDuration を継続的に取り出したいときに向いています。たとえば計測値を独自のロガーに送って推移を追う、といった自動化はコンポーネント版でしか行えません。「まず DevTools で探し、狙いを定めた範囲を <Profiler> で数値化する」という組み合わせが実用的です。

まとめ

<Profiler> は、計測したいコンポーネントツリーを idonRender を指定して囲むことで、レンダリングのコストをプログラム的に取得できる React のビルトイン API です。onRender には id / phase(”mount” / “update” / “nested-update”)/ actualDuration / baseDuration / startTime / commitTime の6引数が渡り、特に actualDurationbaseDuration の比較でメモ化の効き具合を判断できます。複数の Profiler は異なる id で並列に、あるいは入れ子にして使えます。計測にはオーバーヘッドがあるため本番ビルドでは既定で無効で、計測するにはプロファイリング用ビルドが必要です。手軽にボトルネックを探すなら DevTools の Profiler タブ、特定範囲の数値を継続的に取り出すなら <Profiler> コンポーネント、と使い分けるとよいでしょう。

参考ページ