Performance APIは、実際のユーザー機器やネットワーク接続で、本番ウェブアプリケーションの応答性を測定してくれます。これによって、クライアント側とサーバー側の両方のコードのボトルネックを特定することができます。
- user timing:クライアント側のJavaScript関数の独自パフォーマンス計測
- paint timing:ブラウザレンダリングのメトリクス
- resource timing:アセットとAjaxコールの読み込みパフォーマンス
- navigation timing:リダイレクト、DNSルックアップ、DOMレディネス等を含むページ読み込みのメトリクス
Performance APIを使用すると、パフォーマンス評価に関する以下のような典型的な問題に対処できます。
- 開発者はアプリケーションのテスト時に、しばしば高速ネットワークに接続されたハイエンドPCを使用します。DevToolsを利用して低速のデバイスをエミュレートできますが、ユーザーの多くが直面する現実の問題、たとえば「2年前のモバイル機器で空港のWiFiに接続する」などを想定することはできません。
- Googleアナリティクスなどのサードパーティサービスはしばしばブロックされ、誤った結果と仮定につながります。また、国によってはプライバシーに関連する問題が発生することも。
- Performance APIは、様々なメトリクスを、
Date()
などの方式よりも正確に測定します。
以下のセクションでは、Performance APIの使用方法についてご紹介します。なお、JavaScriptとページ読み込みのメトリクスについてある程度の知識をお持ちであることを前提にご説明していきます。があることを想定します。
Performance APIの利用可能性
ほとんどのブラウザは、Performance APIをサポートしており、これにはIE 10とIE 11も含まれ、IE 9でさえ限定的ですがサポートしています。Performance APIの有無は、以下の方法で確認できます。
if ('performance' in window) {
// use Performance API
}
Performance APIは完全にPolyfillできないため、サポートしないブラウザには注意が必要です。たとえば、90%のユーザーがInternet Explorer 8で快適にブラウジングしている環境では、わずか10%のユーザーしか高機能アプリケーションで測定できないことになります。
Performance APIはWeb Workerで使用でき、ブラウザの操作を停止することなく、バックグラウンドのスレッドで複雑な計算を実行することができます。
標準のperf_hooksモジュールを使用すると、サーバーサイドのNode.js内で、Performance APIのほとんどのメソッドを使用できます。
// Node.js performance
import { performance } from 'node:perf_hooks';
// or in Common JS: const { performance } = require('node:perf_hooks');
console.log( performance.now() );
Denoでも標準Performance APIが利用できます。
// Deno performance
console.log( performance.now() );
高精度の時間計測を有効にするには、--allow-hrtime
パーミッションでスクリプトを実行する必要があります。
deno run --allow-hrtime index.js
サーバーサイドのパフォーマンスは、負荷、CPU、RAM、ハードディスク、クラウドサービスの制限に依存するため、通常、評価も管理も簡単です。コードをリファクタリングするよりも、ハードウェアのアップグレードや、PM2、クラスタリング、Kubernetesなどのプロセス管理の方が効果的な場合があります。
この理由から、次の章では、クライアント側のパフォーマンスに焦点を当てます。
独自のパフォーマンス測定
Performance APIを使用すると、アプリケーション関数の実行速度を計測できます。過去にDate()
を使用して時間を計測するコードを書いた、あるいは見たことがある方もいるかもしれません。
const timeStart = new Date();
runMyCode();
const timeTaken = new Date() - timeStart;
console.log(`runMyCode() executed in ${ timeTaken }ms`);
Performance APIには、大きく2つのメリットがあります。
- 精度:
Date()
は、ミリ秒単位で計測するが、Performance APIは、ミリ秒以下で計測可能(ブラウザによる)。 - 信頼性:ユーザーやOSがシステム時刻を変更できるため、
Date()
ベースのメトリクスは常に正確とは限らない。時計が進むと関数が特に遅く見える。
Date()
に相当する関数がperformance.now()
です。ドキュメントを作成するプロセスの開始時(ページの読み込み時)に0に設定され、高精度の時刻を返します。
const timeStart = performance.now();
runMyCode();
const timeTaken = performance.now() - timeStart;
console.log(`runMyCode() executed in ${ timeTaken }ms`);
非標準のperformance.timeOrigin
プロパティも、1970年1月1日からの時間を返しますが、IEとDenoでは使用できません。
またperformance.now
()も、数回以上の計測を行う場合には、実用的ではありません。Performance APIではバッファが提供されていて、performance.mark()
にラベル名を渡すことで、後の解析用にイベントを記録できます。
performance.mark('start:app');
performance.mark('start:init');
init(); // run initialization functions
performance.mark('end:init');
performance.mark('start:funcX');
funcX(); // run another function
performance.mark('end:funcX');
performance.mark('end:app');
パフォーマンスバッファ内のすべてのマークオブジェクトの配列は、以下のコードで抽出できます。
const mark = performance.getEntriesByType('mark');
以下が結果例です。
[
{
detail: null
duration: 0
entryType: "mark"
name: "start:app"
startTime: 1000
},
{
detail: null
duration: 0
entryType: "mark"
name: "start:init"
startTime: 1001
},
{
detail: null
duration: 0
entryType: "mark"
name: "end:init"
startTime: 1100
},
...
]
performance.measure()
メソッドは、2つのマーク間の時間を計算し、パフォーマンスバッファに保存します。メソッドの引数には、メジャーの名前、開始マーク名(nullなら、ページ読み込み時からの測定)、終了マーク名(nullなら、現在時刻までの測定)を渡します。
performance.measure('init', 'start:init', 'end:init');
計算された時間とともに、PerformanceMeasureオブジェクトがバッファに追加されます。この値を取得するには、すべてのメジャーの配列を要求するか、
const measure = performance.getEntriesByType('measure');
または、メジャー名で要求します。
performance.getEntriesByName('init');
以下が結果例です。
[
{
detail: null
duration: 99
entryType: "measure"
name: "init"
startTime: 1001
}
]
パフォーマンスバッファの使用
パフォーマンスバッファは、マークやメジャーだけでなく、ナビゲーション、リソース、ペイントのタイミング(これについては後述します)の自動的な記録にも使用されます。以下のコードで、バッファ内のすべてのエントリの配列を取得できます。
performance.getEntries();
ほとんどのブラウザには、デフォルトで最大150のリソースメトリクスを保存できるバッファが搭載されています。大体の検証はこれで十分ですが、必要に応じてバッファの上限を変更することができます。
// record 500 metrics
performance.setResourceTimingBufferSize(500);
マークは名前でクリアするか、空の値を指定してすべてのマークをクリアします。
performance.clearMarks('start:init');
同様に、メジャーも名前を指定してクリアするか、空の値を指定してすべてのメジャーをクリアできます。
performance.clearMeasures();
パフォーマンスバッファの変化の監視
PerformanceObserverは、パフォーマンスバッファの変化を監視し、特定のイベントが発生した際に関数を実行できます。DOMの更新に応答するMutationObserverや、要素のビューポートへのスクロールを検出するIntersectionObserverを使用したことがあれば、この構文には馴染みがあるでしょう。
2つのパラメータを持つオブザーバ関数を定義する必要があります。
- 検出されたオブザーバエントリの配列。
- オブザーバオブジェクト。必要であれば、
disconnect()
メソッドを呼び出して、オブザーバを停止することが可能。
function performanceCallback(list, observer) {
list.getEntries().forEach(entry => {
console.log(`name : ${ entry.name }`);
console.log(`type : ${ entry.type }`);
console.log(`start : ${ entry.startTime }`);
console.log(`duration: ${ entry.duration }`);
});
}
新しいPerformanceObserverオブジェクトにこの関数を渡し、observe()
メソッドに、監視するパフォーマンスバッファのentryTypesの配列を渡します。
let observer = new PerformanceObserver( performanceCallback );
observer.observe({ entryTypes: ['mark', 'measure'] });
この例では、新しいマークやメジャーを追加すると、performanceCallback()
関数が実行されます。ここではメッセージをログに出力するだけですが、データのアップロードや追加の計算のトリガーとしても使用できます。
描画パフォーマンスの測定
Paint Timing APIは、クライアント側のJavaScriptでのみ利用可能で、コアウェブバイタルにとって重要な2つのメトリクスを自動的に記録します。
- first-paint:ブラウザがページの描画を開始した。
- first-contentful-paint:ブラウザがDOMコンテンツの最初の重要な項目、例えば、見出しや画像を描画した。
メトリクスは、パフォーマンスバッファから配列に抽出できます。
const paintTimes = performance.getEntriesByType('paint');
値が適切に反映されるよう、ページが完全に読み込まれてから実行してください。window.load
イベントを待つか、PerformanceObserver
を使用して、entryType「paint
」を監視してください。
以下が結果例です。
[
{
"name": "first-paint",
"entryType": "paint",
"startTime": 812,
"duration": 0
},
{
"name": "first-contentful-paint",
"entryType": "paint",
"startTime": 856,
"duration": 0
}
]
first-paintが低速である場合、多くの原因は、レンダリングをブロックするCSSやJavaScriptにあります。first-contentful-paintまでの間隔が大きい場合は、ブラウザは、大きな画像のダウンロードや、複雑な要素のレンダリングを実行している可能性があります。
リソースパフォーマンスの測定
画像、スタイルシート、JavaScriptファイルなどのリソースのネットワークタイミングは、自動的にパフォーマンスバッファに記録されます。ネットワーク速度の問題に対して開発者ができることは、ファイルサイズを小さくする程度。ですが、大きなアセット、低速なAjax応答、パフォーマンスの悪いサードパーティ製スクリプトなどの問題の把握には役立ちます。
PerformanceResourceTimingメトリクスの配列を、バッファから抽出できます。
const resources = performance.getEntriesByType('resource');
もしくは、完全なURLを渡してアセットのメトリクスを取得します。
const resource = performance.getEntriesByName('https://test.com/script.js');
以下が結果例です。
[
{
connectEnd: 195,
connectStart: 195,
decodedBodySize: 0,
domainLookupEnd: 195,
domainLookupStart: 195,
duration: 2,
encodedBodySize: 0,
entryType: "resource",
fetchStart: 195,
initiatorType: "script",
name: "https://test.com/script.js",
nextHopProtocol: "h3",
redirectEnd: 0,
redirectStart: 0,
requestStart: 195,
responseEnd: 197,
responseStart: 197,
secureConnectionStart: 195,
serverTiming: [],
startTime: 195,
transferSize: 0,
workerStart: 195
}
]
以下のプロパティを調査することができます。
- name:リソースURL
- entryType:「resource」
- initiatorType:リソースがどのように開始されたか(「script」「link」など)
- serverTiming:HTTP Server-Timingヘッダでサーバーから渡される
PerformanceServerTiming
オブジェクトの配列(サーバー側アプリケーションは、追加の分析のためにクライアントにメトリクスを送信可能) - startTime:取得を開始したときの時刻
- nextHopProtocol:使用されるネットワークプロトコル
- workerStart:Progressive Web App Service Workerを開始する直前の時刻(Service Workerがリクエストを受け付けない場合は0)
- redirectStart:リダイレクトを開始したときの時刻
- redirectEnd:最後のリダイレクトのレスポンスの最後のバイトを受信した直後の時刻
- fetchStart:リソース取得を開始する直前の時刻
- domainLookupStart:DNSルックアップ直前の時刻
- domainLookupEnd:DNSルックアップ直後の時刻
- connectStart:サーバー接続を確立する直前の時刻
- connectEnd:サーバー接続を確立した直後の時刻
- secureConnectionStart:SSLハンドシェイク直前の時刻
- requestStart:ブラウザがリソースを要求する直前の時刻
- responseStart:ブラウザがデータの最初のバイトを受信したときの時刻
- responseEnd:最後のバイトを受信した、または接続をクローズした直後の時刻
- duration:startTimeとresponseEndの間の差
- transferSize:バイト単位のリソースサイズ(ヘッダーと圧縮されたボディを含む)
- encodedBodySize:解凍直前のバイト単位のリソースボディのサイズ
- decodedBodySize:解凍直後のバイト単位のリソースボディのサイズ
このサンプルスクリプトは、Fetch APIによって開始されたすべてのAjaxリクエストを取得し、転送サイズの合計と転送時間を返します。
const fetchAll = performance.getEntriesByType('resource')
.filter( r => r.initiatorType === 'fetch')
.reduce( (sum, current) => {
return {
transferSize: sum.transferSize += current.transferSize,
duration: sum.duration += current.duration
}
},
{ transferSize: 0, duration: 0 }
);
ナビゲーションパフォーマンスの測定
直前のページのアンロードと、現在のページのロードのネットワークタイミングは、1つのPerformanceNavigationTiming
オブジェクトとして、自動的にパフォーマンスバッファに記録されます。
これを配列に取り出すには、以下のコードを使用するか、
const pageTime = performance.getEntriesByType('navigation');
または、ページのURLを.getEntriesByName()
に渡します。
const pageTiming = performance.getEntriesByName(window.location);
メトリクスは、リソースと同じですが、ページ固有の値もあります。
- entryType:例:「navigation」
- type:「navigate」「reload」「back_forward」「prerender」のどれか
- redirectCount:リダイレクトの数
- unloadEventStart:前のドキュメントのunloadイベント直前の時刻
- unloadEventEnd:前のドキュメントのunloadイベント直後の時刻
- domInteractive:ブラウザがHTMLをパースし、DOMを構築したときの時刻
- domContentLoadedEventStart:ドキュメントのDOMContentLoadedイベントが発火する直前の時刻
- domContentLoadedEventEnd:ドキュメントのDOMContentLoadedイベントが完了した直後の時刻
- domComplete:DOMの構築、およびDOMContentLoadedイベントの完了直後の時刻
- loadEventStart:ページのloadイベントが発火する直前の時刻
- loadEventEnd:ページのloadイベントが完了し、すべてのアセットが利用可能になった直後の時刻
典型的な問題と主な原因としては、以下が挙げられます。
- unloadEventEndとdomInteractiveの間の長時間の遅延。サーバーの応答が遅い。
- domContentLoadedEventStartとdomCompleteの間の長時間の遅延。ページの開始時のスクリプトが遅い。
- domCompleteとloadEventEndの間の長時間の遅延。ページのアセットが多すぎるか、いくつかのアセットのロードに時間がかかりすぎている。
パフォーマンスの記録と分析
Performance APIを使用すると、実際のデータを照合し、サーバーにアップロードして分析することができます。Googleアナリティクスのようなサードパーティのサービスを使用してもデータを保存できますが、サードパーティのスクリプトはブロックされたり、新たなパフォーマンスの問題を引き起こす可能性が。自作のソリューションであれば、モニタリングが他の機能に影響を与えないように、要件に合わせて調整することが可能です。
統計情報を取得できない可能性には、ご注意ください。例えば、ユーザーが古いブラウザを使用していたり、JavaScriptをブロックしていたり、企業のプロキシを経由している場合です。不完全な情報に基づいて推測する前に、どのようなデータが欠けているのかを把握するのが得策です。
理想的には、分析スクリプトでは、パフォーマンスに影響を与える複雑な計算や大量データのアップロードを避けてください。Web Workerを利用し、同期的なlocalStorageの呼び出しを最小限にしましょう。生データは、いつでも後でバッチ処理できます。
最後に、非常に速いデバイスや、非常に遅い接続など、統計に影響を及ぼす異常値にも目を向けるようにしてください。たとえば、9人のユーザーが2秒でページを読み込み、10人目のユーザーが60秒かけてダウンロードした場合、平均レイテンシは約8秒になります。より現実的な指標は、中央値(2秒)、または90パーセンタイル(10人中9人のユーザーで読み込み時間が2秒以下)です。
まとめ
ウェブパフォーマンスは、依然として開発者にとって重要な要素です。ユーザーは常に、サイトやアプリケーションがほとんどのデバイスで適切に応答することを期待するものです。また、遅いサイトはGoogleで低い検索順位になるため、検索エンジン最適化も影響を受けます。
多くのパフォーマンス監視ツールがありますが、ほとんどはサーバーサイドの実行速度を評価するか、限られた高機能クライアントを使用してブラウザの描画を測定します。Performance APIを使用すると、他の方法では計算できない、リアルなユーザーメトリクスを照合できます。
コメントを残す