Claude Media
Claude CodeでINPを改善 — 重いイベントハンドラの特定と分割

Claude CodeでINPを改善 — 重いイベントハンドラの特定と分割

Chrome DevTools MCPで重いクリック処理のトレースを取らせ、scheduler.yieldで分割し、同じ条件で再計測して効果を確かめる手順です。

クリックしてから画面が変わるまでが遅いページは、Claude Codeに「トレースを取る → 重い処理を見つける → 分割する → 取り直す」の一巡を任せられます。計測の手はChrome DevTools MCPが担い、Claude Codeは結果を読んでコードを直します。

この記事では、INP(Interaction to Next Paint)を悪くしているイベントハンドラを対象に、依頼の出し方、分割コードの形、再計測の条件をそろえるところまでを順に書きます。MCPサーバーの導入そのものはChrome DevTools MCPの導入手順にあるので、ここでは最小限に留めます。

INPが悪いとき、直す対象はどこか

INPは、ページ上のすべての操作を見て、遅かった操作の値を報告する指標です。75パーセンタイルで200ミリ秒以下なら良好、500ミリ秒を超えると不良とされます。

1回の操作は3つの区間に分かれます。

  • 入力遅延: 操作を受けてから、最初のイベントハンドラが動き出すまで
  • 処理時間: すべてのイベントハンドラが動く時間
  • 表示遅延: ハンドラが終わってから、次のフレームが描画されるまで

イベントハンドラの分割で縮まるのは、主に処理時間です。別の長いタスクが先に走っていて入力遅延が大きい場合や、描画が重い場合は、ハンドラを割っても効きません。どの区間が長いかを先に見るのが、この手順の最初の分かれ目になります。

Claude Codeに計測の手を持たせる

Chrome DevTools MCPは、トレースの記録とインサイトの取得、クリックなどの操作、CPUの絞り込みを1つのMCPサーバーから呼べるようにします。Claude Codeへの登録は、stdioサーバーの書式どおりです。

claude mcp add chrome-devtools --scope user \
  -- npx -y chrome-devtools-mcp@latest

--より後ろがサーバーの起動コマンドで、--scope userは全プロジェクトで使う指定です。チームで共有するなら--scope projectにすると.mcp.jsonに書かれ、各自が初回に承認します。

公式が対応を明記しているのはGoogle ChromeとChrome for Testingです。ほかのChromium系ブラウザは動く場合もありますが、保証はありません。

重いハンドラをトレースで特定させる

操作に紐づくトレースは、ページを開いた状態で記録を始め、そのあとクリックし、記録を止める順で取ります。リロード付きのトレースは読み込み時の性能を見るもので、クリック後の応答を見るには向きません。

手順

クリック後の応答を測る流れ

  1. 1

    ページを開き、操作対象の要素を特定する

    navigate_pageでURLを開き、take_snapshotで要素のuidを得ます。clickはこのuidを受け取ります。

  2. 2

    CPUを絞る

    emulateのcpuThrottlingRateに4などを渡します。手元の速いマシンでは長いタスクが表に出ないことがあるため、遅い端末に寄せます。

  3. 3

    記録を始め、クリックし、止める

    performance_start_traceはreloadを付けずに呼び、clickを1回挟んでperformance_stop_traceで止めます。filePathを渡せば生のトレースも残せます。

  4. 4

    インサイトを掘り下げる

    返ったインサイトのうち、対象の操作に関するものをperformance_analyze_insightで開きます。

CPUの絞り込みを入れるのは、DevToolsの診断ガイドも長いタスクを確実に表に出すためにCPUスロットリングか実機の接続を勧めているからです。

依頼文は、手順と報告の形を指定すると読み取りやすくなります。たとえば次のように書きます。

http://localhost:3000/products を開き、「絞り込み」ボタンのクリックを調べて。
CPUは4倍遅くして、リロードなしでトレースを取って。
入力遅延・処理時間・表示遅延のうち、どれが一番長いかを先に答えて。
処理時間が長い場合だけ、該当するイベントハンドラの関数名とファイルを挙げて。

「どの区間が長いか」を最初に答えさせる点が肝です。処理時間でないと分かれば、分割に進まず別の原因を調べる判断ができ、無駄な書き換えを避けられます。

分割案を出させるときの指定

処理時間が長いと分かったら、ハンドラのコードを読ませて分割案を出させます。型は、画面に見えるべき更新を先に済ませ、見えない処理を後ろへ回すことです。

次は、設定保存の例を公式ガイドの形に沿って書いたものです。

// 分割前: 5つの関数が1つの長いタスクになる
function saveSettings() {
  validateForm();
  showSpinner();
  saveToDatabase();
  updateUI();
  sendAnalytics();
}
 
// 分割後: 画面に関わる処理だけ先に走らせる
async function saveSettings() {
  validateForm();
  showSpinner();
  updateUI();
  await yieldToMain();
  saveToDatabase();
  sendAnalytics();
}
 
function yieldToMain() {
  if (globalThis.scheduler?.yield) {
    return scheduler.yield();
  }
  return new Promise((resolve) => setTimeout(resolve, 0));
}

scheduler.yield()の続きは優先的に再開されるため、途中でほかのタスクに割り込まれにくい利点があります。setTimeoutで譲ると続きがキューの末尾に回り、先に待っているタスクが先に走ります。入れ子のsetTimeoutが5回を超えると、1回ごとに最低5ミリ秒の遅延も入ります。

ただし、scheduler.yield()はすべてのブラウザで使えるわけではありません。web.devの対応表ではChromeとEdgeが129、Firefoxが142からで、Safariは対応していません。MDNでもBaselineではないと位置づけられています。上のコードのようにフォールバックを必ず付けるよう、依頼文で指定してください。

配列を回すループは、時間で譲る

大量の要素を処理するループでは、1件ごとに譲ると譲る回数のほうが処理より重くなります。公式の例は、前回譲ってから50ミリ秒を超えたときだけ譲る形です。

async function runJobs(jobQueue, deadline = 50) {
  let lastYield = performance.now();
  for (const job of jobQueue) {
    job();
    if (performance.now() - lastYield > deadline) {
      await yieldToMain();
      lastYield = performance.now();
    }
  }
}

ここでforEachに書き換えさせないでください。forEachは各回のPromiseを待たないため、awaitが意図どおりに効きません。公式ガイドも同じ注意を挙げています。この種の約束事は、CLAUDE.mdに書いておくとセッションをまたいで守られます。

## INP改善の作業ルール
- 計測はchrome-devtools MCPで行い、CPUの絞り込みは毎回同じ値にする
- 長いタスクの分割は共通のyieldToMain()を使い、setTimeoutを直書きしない
- ループでは譲る間隔を時間(50ms目安)で決め、forEachにawaitを入れない
- 変更の前後で同じ操作のトレースを取り、各区間の時間を表で報告する

yieldToMainのような共通関数を先に置いてあれば、Claude Codeは各ハンドラで同じ形を再利用でき、差分も小さく済みます。CLAUDE.mdは長くなるとコンテキストを圧迫し警告も出るので、ルールは短く保ちます。目安はCLAUDE.md is too long警告の直し方にまとめています。

同じ条件で取り直して効果を確かめる

分割したら、最初と同じ依頼文でトレースを取り直します。条件が違うと前後を比べられません。そろえるのは次の4点です。

そろえる項目理由
対象ページと操作理由別の操作と比べても意味がない
cpuThrottlingRateの値理由絞り込みが違えば処理時間が変わる
リロードの有無理由読み込み時のトレースとは別物になる
操作前のページの状態理由入力済みの内容などで処理量が変わる

報告させるのは、入力遅延・処理時間・表示遅延の3区間の前後比較です。処理時間が縮み、表示遅延が変わっていなければ、分割が効いたと読めます。逆に処理時間が縮んだのに全体が変わらないなら、残りの区間に原因があります。

計測のばらつきを抑えるため、同じ条件で数回取って最も遅いものか中央付近の値を見る指定も有効です。1回のトレースだけで「直った」と判断させない運用が安全です。

Claudeの読みを手元のDevToolsで照合する

トレースの読み取りはClaude Codeに任せきりにせず、結論だけでも手元のDevToolsで確かめると安心です。同じ操作をPerformanceパネルで記録すると、上部の概要に長いタスクが赤い帯で出ます。操作のあたりを拡大し、Interactionsトラックにカーソルを重ねると、その操作の内訳がツールチップに出ます。

左のひげが入力遅延、ひげに挟まれた実線の部分が処理時間、右のひげが表示遅延です。Claude Codeが「処理時間が最長」と答えたなら、この実線がいちばん長く見えるはずです。食い違うときは、記録した操作が別物だったり、リロードの有無が違ったりしていることが多く、依頼文の条件を見直す合図になります。

どの操作が遅いのかがそもそも分からないときは、パネルを開いた直後に出るライブの指標ビューが入口になります。いくつか操作を試すと、Interactionsのログに内訳つきで並び、INPになった操作が強調されます。読み込み中の応答性の目安には、Total Blocking Timeがよく相関すると説明されています。

直らないときの切り分け

分割しても改善しないときは、次の順で原因を確かめます。

切り分け

分割しても INP が下がらないとき

  1. Q処理時間が最も長い区間でしたか?
    答えがいいえのとき

    入力遅延が長いなら、操作の直前に走っている別の長いタスクを探します。初期化処理やサードパーティのスクリプトが典型です。

    答えがはい
  2. Q表示遅延が最も長い区間でしたか?
    答えがいいえのとき

    表示遅延が長い場合は、レイアウトやスタイル計算、DOMの大きさが原因です。ハンドラの分割では縮まらないので、描画側の最適化に進みます。

    答えがはい

ハンドラの分割で改善が見込める

処理時間が主因です。画面に必要な更新を先に済ませ、残りを後ろのタスクへ回す形を見直します。

ラボでの数値は、実ユーザーのINPとは別物です。INPの判定は実ユーザーの75パーセンタイルで行われるため、手元のトレースで速くなっても、フィールドデータに反映されるまでには時間がかかります。まず実ユーザーのデータで遅い操作を見つけ、ラボで再現して直す、という順が公式の想定です。

Claude Code側のつまずきとして、トレースの結果が大きいとMCPの出力が上限に当たることがあります。出力が10,000トークンを超えると警告が出て、既定では25,000トークンで切られます。切られたときはMAX_MCP_OUTPUT_TOKENSを上げるか、performance_analyze_insightで見たいインサイトだけに絞って呼ばせます。

依頼の型をひとつにまとめる

次の一文を雛形にしておくと、毎回同じ流れで進みます。

{URL}の「{ボタン名}」クリックについて、CPU4倍・リロードなしでトレースを取り、
3区間のどれが長いかを答えて。処理時間が原因なら該当ハンドラを分割し、
同じ条件で取り直して、前後の3区間を表で並べて。

計測が主、書き換えが従という順序を守ることで、感覚ではなく数値に基づいた改善になります。分割後の差分は小さく保たれ、レビューもしやすくなります。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. web.dev/articles/optimize-long-tasks
  2. web.dev/articles/inp
  3. web.dev/articles/diagnose-slow-interactions-in-the-lab
  4. github.com/ChromeDevTools/chrome-devtools-mcp
  5. code.claude.com/docs/en/mcp
この記事を共有:XはてブLinkedIn