WebPagetest を使わずに Speed Index を算出する

題の通り。

WebPagetest を使うと、Speed Index というページのコンテンツがいかに速くレンダリングされたかの総合的なスコアが算出できます。Speed Index は、定期的にこのブログで取り上げていますが、算出方法の内容は https://github.com/t32k/webpagetest-doc-ja/blob/master/using-webpagetest/metrics/speed-index/index.md に詳しく書かれています。( Speed Index とは異なりますが、First Paint のタイミングを計測するための Web 標準は GitHub - WICG/paint-timing: A proposal for a Time To First Paint specification. で議論されているようです )

個人的な興味で、とあるウェブアプリの2つの実装方法のうち、どちらが速くレンダリングされるか調べたくなり、Speed Index が指標として適切だと判断しました。Speed Index を算出するために、WebPagetest のページを何回もポチポチやったり、API を呼んだりしてもよかったんですが、なんとなく WebPagetest のリモートのマシンではなく、ローカルのマシンのブラウザでページが表示されるのを見つつ、算出したかったという次第です。ようするに気持ちの問題です。

たまたま、https://github.com/GoogleChrome/lighthouse という Google が提供している Progressive Web App にアプリが即しているかチェックするツールで Speed Index を算出していることに気づいたので、コードを読んだり、grep して https://github.com/pmdartus/speedline を使って Speed Index を算出していることが分かりました。speedline の README に書かれている通りですが、Chrome の Timeline のレコードが記録された JSON を渡せば算出できそうです。なぜ、Timeline のレコードを使うのかは、Speed Index の算出方法を解説した記事の「描画イベントによるVisual Progress」を参照してください。

Timeline のトレースは、https://github.com/cyrus-and/chrome-remote-interface を利用して、Remote Debugging Protocol を喋ることで可能です。chrome-remote-interface は、2年前にこのブログでも紹介していました( chrome-remote-interface を試してみた - kitak.blog )。これ、2年前か…。 chrome-remote-interface を使った Timeline のトレースは、GitHub - paulirish/automated-chrome-profiling: Node.js recipe for automating javascript profiling in Chrome のコードを参考にしました。他にも CPU のプロファイルを取ったりといった各種パフォーマンスメトリクスの収集のサンプルコードがあるので、頭の片隅に置いておくとよさそうです。

Speed Index を算出する手順

材料が揃ったので、手順を示します。

適当に npm でプロジェクトを作成して(npm init)、chrome-remote-interface と speedline をインストールします(npm install chrome-remote-interface --save, npm install -g speedline)。グローバルにインストールしたくない場合は、--save でインストールして、npm-scripts から呼び出すようにしてください。

Debbuging port を開いて Chrome を起動します。Macだと、/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 --user-data-dir=$TMPDIR/chrome-profiling --no-default-browser-check というコマンドです。

以下のスクリプトを用意し、node で実行します。

const fs = require('fs');
const Chrome = require('chrome-remote-interface');

const TRACE_CATEGORIES = ["-*", "devtools.timeline", "disabled-by-default-devtools.timeline", "disabled-by-default-devtools.timeline.frame", "toplevel", "blink.console", "disabled-by-default-devtools.timeline.stack", "disabled-by-default-devtools.screenshot", "disabled-by-default-v8.cpu_profile"];

const rawEvents = [];

Chrome(function (chrome) {
    with (chrome) {
        Page.enable();
        Tracing.start({
            "categories": TRACE_CATEGORIES.join(','),
            "options": "sampling-frequency=10000"  // 1000 is default and too slow.
        });
        Page.navigate({'url': '自分が Speed Index 値を算出したいサイトの URL に置き換える'})
        Page.loadEventFired(function () {
           Tracing.end()
        });

        Tracing.tracingComplete(function () {
            var file = 'profile-' + Date.now() + '.devtools.trace';
            fs.writeFileSync(file, JSON.stringify(rawEvents, null, 2));
            console.log('Trace file: ' + file);
            chrome.close();
        });

        Tracing.dataCollected(function(data){
            var events = data.value;
            rawEvents = rawEvents.concat(events);
        });
    }
}).on('error', function (e) {
    console.error('Cannot connect to Chrome', e);
});

トレースした内容が profile-1482725139003.devtools.trace のようなファイル名で保存されるので、speedline コマンドに食わせて Speed Index を算出させます。 (speedline ./profile-1482725139003.devtools.trace)

First Visual Change: 50 Visually Complete: 1586

Speed Index: 1177.4 Visual Progress: 0=0%, 50=77%, 161=17%, 330=17%, 1420=60%, 1507=72%, 1586=100%

Perceptual Speed Index: 440.0 Perceptual Visual Progress: 0=0%, 50=91%, 161=72%, 330=72%, 1420=73%, 1507=86%, 1586=100%

ヒストグラムを表示する場合は、speedline ./profile-1482725139003.devtools.trace --pretty

f:id:kitak:20161226191109p:plain

単純に算出された Speed Index だけ扱いたい場合は speedline の JavaScript API を以下のようなスクリプトで呼び出します。

const speedline = require('speedline');

speedline('./profile-1482725139003.devtools.trace').then((results) => {
  console.log('Speed Index value:', results.speedIndex);
});

意外と簡単にできました。何回か算出をおこないましたが、Timeline のトレースがうまくできないケースもあるようです(原因はちょっと不明)。今回はツールの使い方だけでしたが、算出方法を解説しながら、speedline のコードリーディングをしてもよさそうですね。