タイムラインは、サイトやアプリの実行でブラウザが行ったさまざまなことについて知見を得るものです。これは、サイトを実行するときにブラウザが行ったことはさまざまな種類 (JavaScript を実行、レイアウトを更新など) に分類でき、またある時点でブラウザはそれらのいずれかを行っているという考え方に基づいています。
よって、例えばフレームレートが落ち込むなどパフォーマンスの問題の兆候があるときはタイムラインを開いて、記録中のある時点でブラウザが何を行っていたかを知ることができます。
X 軸に沿って時間を表します。記録した操作はマーカーと呼び、ブラウザが実行した操作の連続性を反映する滝のような配置で、水平方向のバーとして表示します。
マーカーを選択すると、詳細情報を右側のサイドバーで表示します。そこではマーカーの長さや、マーカーの種類に特有の詳細情報を表示します。
マーカー
操作を示すマーカーは、色分けおよび名前付けされています。以下の操作を記録します:
名称および説明 | 色 | 詳細情報 |
---|---|---|
DOM イベントへの応答として実行する JavaScript コード。 |
|
|
ページ内で実行される JavaScript 関数は、関数が呼び出された理由に応じて名前付けします: Script Tag |
|
|
HTML パース ページの HTML をパースするために費やした時間。 |
|
|
XML パース ページの XML をパースするために費やした時間。 |
|
|
CSS 再計算 ページの要素に適用する算出スタイルを算出する。 |
|
|
レイアウト ページの要素の位置やサイズを計算する。この操作は、"リフロー" と呼ばれることがあります。 |
||
描画 スクリーンにピクセルを描画する。 |
||
ガベージコレクション ガベージコレクションイベント。ノンインクリメンタル GC イベントには "(非インクリメンタル)" を付加する。 |
Firefox 46 の新機能: GC イベントがメモリ割り当ての圧力によって発生した場合は、"Show Allocation Triggers" というリンクを表示します。このリンクをクリックすると、GC イベントの原因になったメモリ割り当てのプロファイルを表示します。 詳しくはメモリ割り当てとガベージコレクションをご覧ください。 |
|
サイクルコレクション C++ の参照カウントを持つデータ構造を回収します。 ガベージコレクションに似ていますが、こちらは C++ のオブジェクト向けです。Kyle Huey のサイクルコレクションに関するブログ記事をご覧ください。 |
|
|
CC グラフ削減 サイクルコレクションの準備や事前の最適化です。 |
|
|
コンソール
|
|
|
タイムスタンプ
|
|
|
DOMContentLoaded ドキュメントの |
||
Load ドキュメントの |
||
メインスレッドの Worker イベント メインスレッドが Worker にメッセージを送信したとき、または Worker からメッセージを受け取ったときに表示します。 |
以下のいずれか:
|
|
Worker スレッドの Worker イベント Worker がメインスレッドからメッセージを受け取ったとき、またはメインスレッドにメッセージを送信したときに表示します。 |
以下のいずれか:
|
マーカーとその色はタイムラインツールとタイムラインの概要で同じであり、双方を関連づけることが容易になります。
マーカーのフィルタリング
ツールバーのボタンを使用して、表示するマーカーを制御できます:
タイムラインのパターン
タイムラインがどう見えるかは、サイトで行っていることの種類に強く依存します。JavaScript を多く使用するサイトでは橙色が多く表示され、視覚的に動的なサイトでは紫色や緑色が多くなるでしょう。それでもパフォーマンスの問題がある可能性を警告する、一般的なパターンがあります。
タイムラインの描画
タイムラインビューでよく見るパターンは、以下のようなものです:
これは、ブラウザがイベントへの応答としてページの更新を行うときの基本的なアルゴリズムを視覚しています:
- JavaScript 関数呼び出し: DOM イベントなどのイベントが、ページ内の JavaScript を実行します。JavaScript は、ページの DOM や CSSOM を変更します。
- スタイルを再計算: ページの要素の算出スタイルが変化したとブラウザが判断した場合は、それらを再計算しなければなりません。
- レイアウト: 続いて、要素の位置や形状を計算するために算出スタイルを使用します。この操作は "レイアウト" と名付けられていますが、"リフロー" とも呼ばれます。
- 描画: そして、ブラウザはスクリーンに要素を再描画しなければなりません。最後のステップはこの流れで示していません。ページは複数のレイヤーに分割され、それぞれを独立して描画した後に、"コンポジション" と呼ばれるプロセスで合成されます。
この流れが完了しなければスクリーンを更新できませんので、ひとつのフレーム内に一連の操作を収めなければなりません。毎秒 60 フレームが、アニメーションがスムーズに見えるレートとして広く受け入れられています。毎秒 60 フレームのレートのために、ブラウザが一連の操作を実行する時間として 16.7 ミリ秒が与えられます。
応答性のために重要なこととして、ブラウザは必ずしもすべてのステップを通らなければならないわけではありません:
- CSS アニメーションは JavaScript を実行する必要なくページを更新します。
- 変更してもリフローを発生させない CSS プロパティがあります。オブジェクトの形状や位置を変更できるプロパティ、例えば
width
、display
、font-size
、top
などを変更すると、リフローが発生します。一方、形状や位置を変更しないプロパティ、例えばcolor
やopacity
などを変更してもリフローは発生しません。 - 変更しても再描画を発生させない CSS プロパティがあります。特に
transform
プロパティを使用して要素をアニメーションさせる場合、ブラウザはトランスフォームを行う要素を別のレイヤーに分離して、要素が移動しても再描画を行う必要がなくなります。要素の新しい位置は、コンポジションで制御されます。
CSS プロパティのアニメーションの記事では、さまざまな CSS プロパティのアニメーションによるパフォーマンスの違いを示すとともに、タイムラインがそれらをどのように知らせるかを説明します。
妨害する JavaScript
デフォルトで JavaScript を実行するスレッドは、ブラウザがレイアウトの更新、再描画、DOM イベントなどに使用するスレッドと同じです。これは、長い間実行する JavaScript 関数が不応答性 (jank) を引き起こす可能性があるということです。アニメーションがぎこちない、あるいはサイトがフリーズするでしょう。
フレームレートツールとタイムラインを組み合わせると、長い間実行する JavaScript が応答性の問題を引き起こしていることが簡単にわかります。以下のスクリーンショットでは、フレームレートの落ち込みを引き起こしている JS 関数を拡大表示しています:
集約的な JavaScript の記事では、長大な JavaScript 関数によって引き起こされる応答性の問題をタイムラインがどのように目立たせるか、またメインスレッドの応答性を維持するために非同期メソッドをどのように使用できるかを説明します。
高コストな描画
box-shadow
など負担が大きい描画効果があり、特に毎フレーム計算が必要なトランジションに適用した場合に顕著です。特に、グラフィックを集中的に扱う操作でフレームレートの落ち込みがみられる場合は、タイムラインで緑色のマーカーがないか確認してください。
ガベージコレクション
タイムラインで赤色のマーカーはガベージコレクション (GC) のイベントを表しており、このとき SpiderMonkey (Firefox の JavaScript エンジン) は到達不能なメモリを探すためにヒープを確認して、そのようなメモリを解放します。GC を実行している間は JavaScript エンジンを一時停止しなければならないので、プログラムが休止して完全に応答しない状態になるため、GC はパフォーマンスと関係性があります。
一時停止する時間を短縮するため、SpiderMonkey はインクリメンタル GC を実装しています。これはガベージコレクションを少しずつ実行でき、合間にプログラムを実行することが可能になります。それでも時にはノンインクリメンタルガベージコレクションが必要であり、この場合プログラムは完了するまで待たなければなりません。
GC イベント、特にノンインクリメンタル GC イベントを避けようとして、特定の JavaScript エンジンの実装に最適化しようとすることは賢くありません。SpiderMonkey は GC がいつ必要か、および特にノンインクリメンタル GC がいつ必要かを検出するために、複雑に組み合わせた発見方法を使用しています。ただし、一般に以下のことが言えます:
- GC は多くのメモリが割り当てられているときに必要です。
- ノンインクリメンタル GC は通常、SpiderMonkey がインクリメンタル GC の実行中にメモリ不足になる可能性があるほどメモリ使用量が高いときに発生します。
タイムラインで GC マーカーを記録するときは、以下の情報を示します:
- GC がインクリメンタルか否か
- GC を実行した理由
- ノンインクリメンタル GC である場合は、インクリメンタルではない理由
- Firefox 46 より、GC イベントがメモリ割り当ての圧力によって発生した場合は、"Show Allocation Triggers" というリンクを表示します。このリンクをクリックすると、GC イベントの原因になったメモリ割り当てのプロファイルを表示します。詳しくはメモリ割り当てとガベージコレクションをご覧ください。
console API でマーカーを追加する
console API を呼び出すことで直接制御できるマーカーが 2 つあります。"コンソール" と "Timestamp" です。
コンソールマーカー
これは、記録内の特定のセクションにマークをつけるものです。
コンソールマーカーを作成するにはセクションの始めで console.time()
を、セクションの終わりで console.timeEnd()
を呼び出します。これらの関数は、セクションの名前として使用される引数を取ります。
例えば、以下のようなコードで考えてみましょう:
var iterations = 70; var multiplier = 1000000000; function calculatePrimes() { console.time("calculating..."); var primes = []; for (var i = 0; i < iterations; i++) { var candidate = i * (multiplier * Math.random()); var isPrime = true; for (var c = 2; c <= Math.sqrt(candidate); ++c) { if (candidate % c === 0) { // not prime isPrime = false; break; } } if (isPrime) { primes.push(candidate); } } console.timeEnd("calculating..."); return primes; }
タイムラインの出力は、以下のようになります:
マーカーは console.time()
に渡した引数で名付けられており、マーカーを選択すると右側のサイドバーでプログラムスタックを確認できます。
Async stack
Firefox 41 の新機能
Firefox 41 より、右側のサイドバーに終了時点、すなわち console.timeEnd()
を呼び出した時点のスタックも表示します。console.timeEnd()
が Promise
の成功によって呼び出された場合は、"(非同期: Promise)" と表示します。これは "async stack" を表すものであり、その時点のコールスタックで promise が生成されています。
例えば、以下のコードで考えてみましょう:
var timerButton = document.getElementById("timer"); timerButton.addEventListener("click", handleClick, false); function handleClick() { console.time("timer"); runTimer(1000).then(timerFinished); } function timerFinished() { console.timeEnd("timer"); console.log("ready!"); } function runTimer(t) { return new Promise(function(resolve) { setTimeout(resolve, t); }); }
タイムラインでは time()
と timeEnd()
の間をマーカーで表示します。このマーカーを選択すると、サイドバーに async stack が表示されるでしょう:
Timestamp マーカー
タイムスタンプで、記録中にその場でマークすることができます。
タイムスタンプマーカーを作成するには、console.timeStamp()
を呼び出します。タイムスタンプのラベルを引数として渡すことができます。
例えば前出のコードで繰り返し 10 回ごとにタイムスタンプを生成して、繰り返し回数を名前として付加します:
var iterations = 70; var multiplier = 1000000000; function calculatePrimes() { console.time("calculating..."); var primes = []; for (var i = 0; i < iterations; i++) { if (i % 10 == 0) { console.timeStamp(i.toString()); } var candidate = i * (multiplier * Math.random()); var isPrime = true; for (var c = 2; c <= Math.sqrt(candidate); ++c) { if (candidate % c === 0) { // not prime isPrime = false; break; } } if (isPrime) { primes.push(candidate); } } console.timeEnd("calculating..."); return primes; }
タイムラインは以下のようになります: