パフォーマンス計測API(PerformanceObserverとTiming)
実利用者の体感をコードで正確に拾えるようになる。Navigation/Resource/Paint/Long Tasks/Elementの計測モデルとPerformanceObserverのバッファ・エントリ型・フィールド計測設計を解説。
- 1.ブラウザは描画パイプラインの各段で計測結果をperformance entry bufferに積み、PerformanceObserverはそれを種別ごとに購読する。buffered:trueで購読前に積まれた過去エントリも一括取得できるのが、scriptより前に起きるNavigation/Paintを取りこぼさない鍵。
- 2.エントリ型はentryTypeで分かれ、PerformanceNavigationTiming・PerformanceResourceTiming・PerformancePaintTiming・PerformanceLongTaskTiming・PerformanceElementTimingがそれぞれ専用フィールドを持つ。時刻はNavigation開始を0とするDOMHighResTimeStamp(ミリ秒・小数)で、systemクロックではなく単調増加のtime originからの相対値。
- 3.フィールド計測はラボと違い回線・端末・操作がバラつくため、observe→sendBeaconでpagehide時にまとめて送り、パーセンタイルで評価するのが定石。getEntriesByTypeはバッファ上限で古いエントリが落ちるので、長時間ページではObserverの逐次受信が安全。
計測の土台:performance timeline とエントリバッファ
Web のパフォーマンス計測APIは、JavaScript で時間を測る仕組みではなく、ブラウザが描画・ネットワーク・タスク処理の各段で記録した結果を後から読み出す仕組みです。レンダリングエンジンやネットワークスタックが内部で計測した値を、種別ごとに performance entry(PerformanceEntry) としてタイムラインに積み、アプリはそれを読みます。基礎となる描画工程は レンダリングパイプライン詳説 を参照してください。
各エントリは共通の4フィールドを持ちます。
name : エントリを識別する文字列(URL や "first-contentful-paint" など)
entryType : 種別("navigation" / "resource" / "paint" / "longtask" ...)
startTime : 開始時刻(time origin からの相対ミリ秒)
duration : 継続時間(ミリ秒。瞬間イベントは 0)
時刻はすべて DOMHighResTimeStamp、すなわち performance.timeOrigin(ナビゲーション開始の時点)を 0 とした**単調増加のミリ秒(小数つき)**です。Date.now() のようなシステム時計ではないため、NTP補正や時刻変更の影響を受けず、区間の引き算が安定します。
PerformanceObserver とバッファの取りこぼし問題
エントリを読む経路は2つあります。performance.getEntriesByType() で今ある分を一括取得する方法と、PerformanceObserver で今後積まれる分を購読する方法です。実務で重要なのは、多くの計測対象がスクリプト実行より前に確定している点です。Navigation や First Paint は、計測コードが走り始める前にすでに記録済みのことが多く、素朴に Observer を張るだけでは取りこぼします。
これを解決するのが buffered: true です。
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// entry.entryType ごとに処理
}
}).observe({ type: "largest-contentful-paint", buffered: true });
buffered: true を付けると、observe を呼ぶ前にバッファへ積まれていた過去エントリも、最初のコールバックでまとめて配られます。これにより「計測コードを後から読み込んでも過去のイベントを拾える」設計になります。
getEntriesByType() が返すのはエントリバッファの中身ですが、このバッファには種別ごとに上限があります(例:resource は既定で250件)。長時間動くページや大量のリソースを読むページでは、古いエントリが押し出されて取得時には消えていることがあります。PerformanceObserver で発生のたびに受け取る方式なら、上限に達して落ちる前に確実に回収できます。バッファを使い切る前に performance.clearResourceTimings() で明示的に空ける手もあります。
observe() の指定方法には2系統あり、{ type: "..." , buffered: ... }(単一種別・buffered指定可)と { entryTypes: ["...", "..."] }(複数種別・buffered不可)が排他です。複数種別を一度に見つつ過去分も欲しいときは、type 指定の observe を種別ごとに複数回呼ぶのが正攻法です。
主要なエントリ型と計測モデル
エントリは entryType で分かれ、それぞれ専用のインターフェース(フィールド集合)を持ちます。
| entryType | インターフェース | 何を測るか |
|---|---|---|
| navigation | PerformanceNavigationTiming | ドキュメント取得〜load までの各段の時刻 |
| resource | PerformanceResourceTiming | 個々のサブリソース取得のフェーズ別時刻 |
| paint | PerformancePaintTiming | FP / FCP の描画時刻 |
| largest-contentful-paint | LargestContentfulPaint | 最大コンテンツ要素の描画時刻 |
| layout-shift | LayoutShift | レイアウトシフトの impact / value |
| longtask | PerformanceLongTaskTiming | 50ms 超のメインスレッド占有 |
| element | PerformanceElementTiming | 明示指定した要素の描画時刻 |
| event / first-input | PerformanceEventTiming | 入力イベントの遅延(INP/FID の素材) |
Navigation Timing
PerformanceNavigationTiming は、メインドキュメントの取得から load までを1本のタイムライン上の時刻列として持ちます。Resource Timing を継承しているため、リダイレクト・DNS・接続・TTFB の各時刻に加えて、domInteractive / domContentLoadedEventStart / loadEventEnd といったドキュメント固有のマイルストーンを含みます。すべて time origin 相対なので、responseStart - requestStart のように段を引き算して各フェーズの所要時間を出します。
const nav = performance.getEntriesByType("navigation")[0];
const ttfb = nav.responseStart; // 受信開始(=TTFB相当)
const dcl = nav.domContentLoadedEventEnd;
const load = nav.loadEventEnd;
Resource Timing
PerformanceResourceTiming は、画像・スクリプト・フェッチなどサブリソース1件ごとのフェーズ別時刻です。同じ段(redirect→DNS→TCP→TLS→request→response)を持ち、ここから接続再利用やキャッシュヒットの有無を読みます。ネットワーク取得そのものの仕組みは Fetch API の内部動作 を参照してください。
別オリジンのリソースは、既定では startTime と duration 以外の詳細フェーズ(DNS・接続・TTFBなど)がすべて0にマスクされます。これは取得タイミングから他サイトの状態を推測する情報漏えいを防ぐためです。詳細を読むには、リソース側のレスポンスに Timing-Allow-Origin ヘッダで自分のオリジンを許可してもらう必要があります。transferSize が 0 ならキャッシュ由来、encodedBodySize と decodedBodySize の差で圧縮効果も読めます。
Paint / Element Timing
paint 種別は FP(first-paint) と FCP(first-contentful-paint) の2エントリを提供し、startTime がそれぞれの描画時刻です。一方 element 種別(Element Timing)は、自分で計測したい要素を elementtiming 属性で明示指定して初めて記録されます。ヒーロー画像や主要見出しの描画時刻を、LCP とは独立に取りたいときに使います。
<img src="hero.webp" elementtiming="hero-image" />
new PerformanceObserver((list) => {
for (const e of list.getEntries()) {
// e.identifier === "hero-image", e.renderTime が描画時刻
}
}).observe({ type: "element", buffered: true });
Long Tasks
longtask は、メインスレッドを 50ms 以上連続で占有したタスクを1件ずつ記録します。50ms という閾値は、その間ユーザー入力に応答できない=知覚可能な詰まり、という基準です。startTime と duration で「いつ・どれだけ詰まったか」を、attribution でおおまかな原因コンテキストを得ます。応答性(INP)が悪化する根本は多くがこの長タスクで、背景の実行モデルは イベントループの内部構造 が土台になります。
Long Tasks 単体は描画指標ではありませんが、入力遅延と表示遅延を引き起こす主因です。TBT(Total Blocking Time)は各 long task の「50ms を超えた分」の総和として定義され、ラボ計測での応答性プロキシになります。フィールドで応答性そのものを測るなら、入力イベント遅延を記録する event / first-input 種別を併用します。各指標の算出ロジックは Core Web Vitals の計測アルゴリズム を参照してください。
フィールド計測の設計
ラボ計測(Lighthouse 等)は端末・回線・操作を固定した再現環境で測りますが、フィールド計測(RUM)は実利用者の多様な条件下の値を集めます。同じ計測アルゴリズムでも入力が違うため値はばらつき、平均ではなくパーセンタイルで評価するのが原則です。
設計の核は「いつ集計し、いつ送るか」です。LCP や CLS や INP はページ滞在の最後まで確定しないため、各操作のたびに送ると最終値が取れません。定石は、Observer で値を更新し続け、ページがバックグラウンド化または破棄される瞬間に一度だけ送ることです。
let lcp = 0;
new PerformanceObserver((list) => {
for (const e of list.getEntries()) lcp = e.startTime; // 最後の候補で上書き
}).observe({ type: "largest-contentful-paint", buffered: true });
addEventListener("pagehide", () => {
navigator.sendBeacon("/rum", JSON.stringify({ lcp }));
}, { once: true });
| 送信の手段 | 性質 | 適否 |
|---|---|---|
| fetch(通常) | ページ破棄でキャンセルされ得る | 離脱時送信には不向き |
| navigator.sendBeacon | 破棄後もブラウザがバックグラウンド送信を保証 | 離脱時の最終送信に最適 |
| fetch(keepalive:true) | beacon 同様に破棄後も送れる | 本文サイズ上限に注意して可 |
送信トリガは unload ではなく pagehide または visibilitychange(hidden化)が推奨です。unload は BFCache を無効化するうえ、モバイルでは発火が保証されないためです。
頻出は次の5点です。(1) 全エントリは time origin 相対の DOMHighResTimeStamp で、name/entryType/startTime/duration を共通に持つ。(2) Observer の buffered: true は observe 前の過去エントリも配り、scriptより前のNavigation/Paintを取りこぼさない。(3) getEntriesByType はバッファ上限で古い分が落ちるので長時間ページはObserver受信が安全。(4) クロスオリジンのResource詳細は Timing-Allow-Origin がないと0にマスクされる。(5) フィールド計測は pagehide で sendBeacon 送信し、パーセンタイルで評価する。
まとめ
パフォーマンス計測APIは、ブラウザが描画・ネットワーク・タスク処理の各段で記録した PerformanceEntry を performance timeline に積み、PerformanceObserver が entryType ごとに購読する仕組みです。時刻はすべて time origin 相対の DOMHighResTimeStamp で、Navigation/Resource/Paint/LongTask/Element の各型が専用フィールドを持ちます。buffered: true は observe 前の過去エントリも配り、scriptより前に確定する指標の取りこぼしを防ぎます。getEntriesByType はバッファ上限で古い分が落ちるため、長時間ページでは Observer の逐次受信が安全です。クロスオリジンの Resource 詳細は Timing-Allow-Origin が必要です。フィールド計測では値を更新し続け、pagehide で sendBeacon により一度だけ送り、パーセンタイルで評価します。各指標の算出は Core Web Vitals の計測アルゴリズム、改善の実務手順は Web パフォーマンス を合わせて押さえてください。
Web/フロントエンド Article
パフォーマンス計測API(PerformanceObserverとTiming)を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
パフォーマンス
比較で見る軸
難易度: advanced / カテゴリ: Web/フロントエンド / タグ数: 5
導入後に効く点
エントリ型はentryTypeで分かれ、PerformanceNavigationTiming・PerformanceResourceTiming・PerformancePaintTiming・PerformanceLongTaskTiming・PerformanceElementTimingがそれぞれ専用フィールドを持つ。時刻はNavigation開始を0とするDOMHighResTimeStamp(ミリ秒・小数)で、systemクロックではなく単調増加のtime originからの相対値。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- Web/フロントエンド
- タグ数
- 5
判断チェックリスト
- 自社の用途が「パフォーマンス / ブラウザ」に近いか確認する。
- 強みである「ブラウザは描画パイプラインの各段で計測結果をperformance entry bufferに積み、PerformanceObserverはそれを種別ごとに購読する。buffered:trueで購読前に積まれた過去エントリも一括取得できるのが、scriptより前に起きるNavigation/Paintを取りこぼさない鍵。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。