カスタム速度 API

ウェブアプリについて理解する

Alex Danilo

優れたユーザー エクスペリエンスを実現するには、高パフォーマンスのウェブ アプリケーションが不可欠です。ウェブ アプリケーションの複雑さが増すなか、魅力的なエクスペリエンスを実現するには、パフォーマンスへの影響を把握することが不可欠です。ここ数年で、ネットワークのパフォーマンスや読み込み時間などの分析に役立つさまざまな API がブラウザに登場してきましたが、これらの API は必ずしも、アプリケーションの速度を低下させている要因を見つけるのに十分な柔軟性を備え、詳細な情報を提供できるとは限りません。ここで登場するのが User Timing API です。この API を使用すると、ウェブ アプリケーションを計測して、アプリケーションで時間のかかる部分を特定できます。この記事では、この API とその使用例について説明します。

測定できないものは最適化できません

遅いウェブ アプリケーションを高速化するための第一歩は、どこに時間を費やしているのかを把握することです。ホットスポットを特定するには、JavaScript コードの領域の時間的影響を測定することが理想的な方法です。パフォーマンスの改善方法を見つけるための第一歩です。幸い、User Timing API を使用すると、JavaScript のさまざまな部分に API 呼び出しを挿入して、最適化に役立つ詳細なタイミング データを抽出できます。

高解像度の時間と now()

正確な時間測定の基本は精度です。以前はミリ秒単位の測定に基づくタイミングで問題ありませんでしたが、ジャンクのない 60 FPS サイトを構築するには、各フレームを 16 ミリ秒で描画する必要があります。そのため、精度がミリ秒単位の場合、適切な分析に必要な精度が不足します。最新のブラウザに組み込まれている新しいタイミング タイプである [高解像度時間] を入力します。High Resolution Time により、マイクロ秒単位の精度を持つ浮動小数点タイムスタンプが得られます。これは、以前よりも 1,000 倍向上しています。

ウェブ アプリケーションの現在の時刻を取得するには、Performance インターフェースの拡張機能である now() メソッドを呼び出します。次のコードは、その方法を示しています。

var myTime = window.performance.now();

PerformanceTiming という別のインターフェースもあります。これは、ウェブ アプリケーションの読み込み方法に関連するさまざまな時間を提供します。now() メソッドは、PerformanceTimingnavigationStart 時間が経過してからの経過時間を返します。

DOMHighResTimeStamp 型

以前は、ウェブ アプリケーションの時間を測定するために、DOMTimeStamp を返す Date.now() などの関数を使用していました。DOMTimeStamp は、ミリ秒の整数値を値として返します。高解像度時刻に必要な精度を実現するため、DOMHighResTimeStamp という新しい型が導入されました。この型は浮動小数点値で、時間もミリ秒単位で返します。ただし、浮動小数点数であるため、値はミリ秒の小数部分を表すことができ、ミリ秒の 1,000 分の 1 の精度を実現できます。

カスタム速度のインターフェース

高解像度のタイムスタンプが取得できたので、ユーザー タイミング インターフェースを使用してタイミング情報を取得しましょう。

User Timing インターフェースには、アプリケーションのさまざまな場所でメソッドを呼び出す関数が用意されており、Hansel and Gretel スタイルのパンくずリストを提供して、どこに時間が費やされているかを追跡できます。

mark() の使用

タイミング分析ツールキットの主なツールは mark() メソッドです。mark() はタイムスタンプを保存します。mark() の非常に便利な点は、タイムスタンプに名前を付けられることです。API は、名前とタイムスタンプを 1 つの単位として記憶します。

アプリケーション内のさまざまな場所で mark() を呼び出すと、ウェブ アプリケーションでその「マーク」に到達するまでにかかった時間を計算できます。

この仕様では、mark_fully_loadedmark_fully_visiblemark_above_the_fold など、マークに関して多くの意味があり、わかりやすい名前が提示されています。

たとえば、次のコードを使用して、アプリが完全に読み込まれた時点のマークを設定できます。

window.performance.mark('mark_fully_loaded');

ウェブ アプリケーション全体に名前付きマークを設定することで、大量のタイミング データを収集し、自由に分析して、アプリケーションが何をいつ行っているかを把握できます。

measure() による測定値の計算

タイミング マークをいくつか設定したら、それらの間の経過時間を調べます。この操作を行うには、measure() メソッドを使用します。

measure() メソッドは、マーク間の経過時間を計算します。また、PerformanceTiming インターフェースで、マークとよく知られたイベント名の間の時間を測定することもできます。

たとえば、次のようなコードを使用して、DOM が完成してからアプリケーションの状態が完全に読み込まれるまでの時間を計算できます。

window.performance.measure('measure_load_from_dom', 'domComplete', 'mark_fully_loaded');

measure() を呼び出すと、設定したマークとは別に結果が保存されるため、後で取得できます。アプリケーションの実行中に時間を保存することで、アプリケーションの応答性が維持されます。また、アプリケーションが処理を完了した後にすべてのデータをダンプして、後で分析できます。

clearMarks() でマークを破棄しています

設定したマークをまとめて削除できると便利な場合があります。たとえば、ウェブ アプリケーションでバッチ実行を行う場合、バッチ実行ごとに最初から開始する必要があります。

設定したマークは、clearMarks() を呼び出すだけで簡単に削除できます。

以下のサンプルコードは、既存のマーカーをすべて消去します。必要に応じて、タイミング ランを再度設定できます。

window.performance.clearMarks();

ただし、すべてのマークを消去したくない場合があります。特定のマーカーを削除する場合は、削除するマーカーの名前を渡すだけです。たとえば、次のコードがあるとします。

window.performance.clearMarks('mark_fully_loaded');

では、最初の例で設定したマークを削除し、設定した他のマークはそのままにします。

測定した値を削除することもできます。そのためのメソッドは clearMeasures() です。clearMarks() とまったく同じように動作しますが、測定値に対してのみ動作します。たとえば、次のコードです。

window.performance.clearMeasures('measure_load_from_dom');

は、上記の measure() の例で作成した測定値を削除します。すべての測定値を削除する場合は、引数なしで clearMeasures() を呼び出すだけなので、clearMarks() と同じように機能します。

タイミング データを取得する

マーカーを設定したり、区間を測定したりすることは良いことですが、ある時点でそのタイミング データを取得して分析を行う必要があります。これも非常に簡単です。PerformanceTimeline インターフェースを使用するだけです。

たとえば、getEntriesByType() メソッドを使用すると、すべてのマーク時間またはすべての測定時間をリストとして取得できるため、反復処理してデータを処理できます。リストは時系列で返されるため、ウェブ アプリケーションでヒットした順にマークを確認できます。

次のコードがあるとします。

var items = window.performance.getEntriesByType('mark');

は、ウェブ アプリケーションでヒットしたすべてのマークのリストを返します。一方、コード:

var items = window.performance.getEntriesByType('measure');

は、作成したすべての測定のリストを返します。

エントリに指定した特定の名前を使用して、エントリのリストを取得することもできます。たとえば、次のコードは

var items = window.performance.getEntriesByName('mark_fully_loaded');

この場合、startTime プロパティの「mark_full_loading」タイムスタンプを含む 1 つのアイテムを含むリストが返されます。

XHR リクエストのタイミング(例)

User Timing API の概要を把握できたので、この API を使用して、ウェブ アプリケーションですべての XMLHttpRequest にかかる時間を分析できます。

まず、すべての send() リクエストを変更して、マークを設定する関数呼び出しを発行します。同時に、成功コールバックを変更して、別のマークを設定し、リクエストに要した時間を測定する関数呼び出しにします。

したがって、通常は XMLHttpRequest は次のようになります。

var myReq = new XMLHttpRequest();
myReq.open('GET', url, true);
myReq.onload = function(e) {
  do_something(e.responseText);
}
myReq.send();

この例では、リクエスト数を追跡し、実行された各リクエストの測定値を格納するために、グローバル カウンタを追加します。この処理を行うコードは次のようになります。

var reqCnt = 0;

var myReq = new XMLHttpRequest();
myReq.open('GET', url, true);
myReq.onload = function(e) {
  window.performance.mark('mark_end_xhr');
  reqCnt++;
  window.performance.measure('measure_xhr_' + reqCnt, 'mark_start_xhr', 'mark_end_xhr');
  do_something(e.responseText);
}
window.performance.mark('mark_start_xhr');
myReq.send();

上記のコードは、送信する XMLHttpRequest ごとに一意の名前値を持つ measure を生成します。リクエストは順番に実行されるものと仮定します。並列リクエストのコードは、順不同で返されるリクエストを処理するにはもう少し複雑にする必要があります。ここでは、読者のために演習として残しておきます。

ウェブ アプリケーションが複数のリクエストを実行したら、以下のコードを使用して、それらをすべてコンソールにダンプできます。

var items = window.performance.getEntriesByType('measure');
for (var i = 0; i < items.length; ++i) {
  var req = items[i];
  console.log('XHR ' + req.name + ' took ' + req.duration + 'ms');
}

まとめ

User Timing API には、ウェブ アプリケーションのあらゆる側面に適用できる優れたツールが多数用意されています。アプリケーションのホットスポットを絞り込むには、ウェブ アプリケーション全体に API 呼び出しを分散させ、生成されたタイミング データを後処理して、時間を費やしている場所を明確に把握します。ただし、お使いのブラウザがこの API をサポートしていない場合はどうすればよいでしょうか。心配はいりません。優れたポリフィルがこちらにありますので、API をエミュレートし、webpagetest.org とも連携できます。この機会にぜひご登録ください。アプリで User Timing API を試して、アプリの高速化方法を探りましょう。ユーザー エクスペリエンスが大幅に向上し、ユーザーに感謝されるはずです。