ブラウザの「心の声」を聴く:Paint Timing APIでUXのボトルネックを暴く
フロントエンド開発の現場で、「なんとなくサイトが重い」という定性的なフィードバックに悩まされたことはないだろうか。Lighthouseのスコアはそこそこなのに、実機で触ると妙な「もっさり感」がある。
その正体を探るために、我々プロはブラウザの「内面」を覗きに行く必要がある。今回は、ブラウザがレンダリングの過程で発する「今、描画したぞ!」という信号をキャッチする、Paint Timing APIの話をしよう。
ブラウザの裏側で起きている「魔法」の正体
ブラウザのレンダリングエンジン(BlinkやWebKit)は、HTMLを受け取ると血眼になってDOMツリーを構築し、同時にCSSOMとマージしてRender Treeを作る。そしてレイアウト計算を済ませ、ようやく画面に画素を並べるわけだが、ここには明確な「節目」がある。
- FCP (First Contentful Paint): ブラウザがDOMの最初の要素(テキスト、画像、canvasなど)を画面に描き出した瞬間。ユーザーが「お、始まったな」と認識する境界線だ。
- LCP (Largest Contentful Paint): ユーザーがメインコンテンツだと認識する「一番デカい要素」が描画された瞬間。これこそが、体感速度の決定版だ。
これらを計測する際、`performance.now()`を適当に仕込んで満足しているなら、それは少しもったいない。ブラウザ自身が提供する高精度なタイムスタンプを活用すべきだ。
実践:Paint Timing APIで「沈黙の時間」を可視化する
Paint Timing APIは、ブラウザが描画の節目に達したときに自動で記録されるパフォーマンスエントリーを覗き見するためのものだ。現場で使える、シンプルかつ堅牢な監視スクリプトの例を提示しよう。
/
- Paint Timing APIを使って、FCP/LCPを監視・記録するユーティリティ
- 監視対象が少ない場合はObserverで十分だが、計測ログをバックエンドに送るならこの形が定石。
/
const observePaintTiming = () => {
// PerformanceObserverは、ブラウザの内部イベントを購読するための強力なAPI
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
// entryTypeが ‘paint’ ならFCP/FPが含まれる
console.log(`[Paint Timing] ${entry.name}: ${entry.startTime.toFixed(2)}ms`);
// ここで得た値をGoogle Analyticsや自前の監視エンドポイントに送る
// navigator.sendBeaconを使うと、ページ離脱時でも確実に送信できる
});
});
// ‘paint’ タイプのイベントを監視対象に登録
observer.observe({ type: ‘paint’, buffered: true });
};
// LCPは ‘largest-contentful-paint’ という別のエントリタイプが必要
const observeLCP = () => {
new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length – 1]; // 最後に更新されたものが最大の要素になる
console.log(`[LCP] ${lastEntry.startTime.toFixed(2)}ms, 要素: ${lastEntry.element?.tagName}`);
}).observe({ type: ‘largest-contentful-paint’, buffered: true });
};
// 実行
observePaintTiming();
observeLCP();
なぜこれが「現場の武器」になるのか
このコードをただコピペするだけでなく、以下のポイントを意識してほしい。
1. `buffered: true` の重要性:
これがないと、Observerを登録した「後」に発生したイベントしか拾えない。スクリプトの読み込みが遅れた場合、肝心の初期描画のタイミングを取りこぼすことになる。このオプションは必須だ。
2. LCPの「更新」を理解する:
LCPは一発で決まらない。最初は小さな見出しが表示され、その後に巨大なヒーロー画像が読み込まれると、LCPのタイミングは更新される。`entries[entries.length – 1]` を参照しているのは、常に「最新かつ最大の描画」を捉えるためだ。
3. ユーザー体験への変換:
計測値は単なる数字ではない。「FCPが遅いならサーバーサイドのTTFB(Time to First Byte)を疑え」「LCPが遅いならメイン画像のプリロードや最適化が足りない」という、具体的な改善アクションに直結させるための羅列なのだ。
最後に:スペックではなく、体験を計測する
ブラウザは非常に賢い。しかし、エンジニアがその「信号」を拾い上げなければ、改善の余地は闇に葬られる。
Paint Timing APIを使いこなすことは、ブラウザというブラックボックスに対して「君は今、何をどれくらいで処理したんだい?」と直接尋ねることと同義だ。開発中のローカル環境でこの数値を眺める癖をつけてほしい。10msの遅延にこだわり抜くその姿勢こそが、最高峰のフロントエンドエンジニアへの第一歩となるはずだ。
さあ、エディタを開いて、君のアプリケーションの「沈黙」を可視化してみよう。

コメント