改善前・改善後を比べるデモを作るとき、改善前のほうにはわざと重い JavaScript を積みました。jQuery とスライダーを defer なしで読んで、スクロールのたびに全要素の位置を測り直す。狙って遅くしています。
ところが TBT(Total Blocking Time)が、5回測って5回とも 0ms でした。長時間タスクは毎回出ているのに、です。
最初は計測ミスを疑いました。ただ、何度測っても 0ms のままです。これは計測ミスではなく、TBT の定義どおりの挙動でした。
計測結果
| 指標 | 実測 |
|---|---|
| モバイルスコア | 57 |
| FCP | 8.5 秒 |
| LCP | 22.2 秒 |
| TBT | 0 ms |
| 長時間タスク1件あたり | 73 〜 156 ms |
計測条件:PageSpeed Insights(モバイル)。低速4G 相当・CPU 4倍低速。5回計測の中央値。同じURLを続けて投げると前回の結果が返るため、URLに使い捨てのクエリを付けて毎回測り直しています。
スコア 57点、LCP 22.2秒。誰が見ても遅いページです。それでも TBT だけは満点でした。
TBT はどこを測っているのか
定義を並べるとこうなります。
- FCP(最初の描画)から TTI(操作可能になるまで)の区間を計測窓とする
- その窓の中で発生した、50ミリ秒を超える処理(長時間タスク)を拾う
- 各タスクについて、50ミリ秒を超えた分だけを足し合わせる
問題は1行目です。窓の外で起きた長時間タスクは、どれだけ長くても数に入りません。ページ全体の合計ではなく、ある区間の合計を見ている指標だった、ということです。
なぜ 0ms になったのか
このデモの FCP は 8.8秒。かなり遅いです。いっぽう jQuery による長時間タスクは、読み込み開始から 2.4〜3.1秒のあたりで発火して、そこで終わっていました。
時間軸に並べると、事情がはっきりします。
0s 2.4s 3.1s 8.8s TTI
| [====長時間タスク====] |─────────────────|
↑ ↑
実際に画面が固まっている区間 TBT の計測窓はここから長時間タスクは、計測窓が始まる5秒以上前に終わっています。なので TBT には1ミリ秒も乗りません。
訪問者の側から見ると、このあいだ何が起きているか。画面は真っ白で、しかもタップにも反応しない。体験としては一番きつい区間です。それが数値には出てこない。
注記
TBT 0ms は「長時間タスクが無い」という意味ではありません。「計測窓の中に無い」という意味です。この2つは別のことなので、レポートを読むときは分けて考えてください。
遅いサイトほど TBT が低く出る
ここまでの話を一般化すると、こうなります。FCP が遅いページほど、計測窓の開始が後ろにずれる。すると序盤に集中している JavaScript の処理が窓の手前に取り残されて、TBT に反映されなくなります。
画像が重くて FCP が遅いページでは、まず起きると思っておいたほうがいいです。
実際、このデモを作っているとき「もっとスコアを落としたい」と思って JavaScript を足したのですが、57点から下に行きませんでした。足しても TBT が動かないので、スコアが反応しない。そういうことだったわけです。
逆向きの誤読もあります。実務ではこちらのほうが厄介です。画像を最適化して FCP を縮めると、計測窓が前にずれて、これまで窓の外にあった長時間タスクが窓の中に入ってくる。画像を直したら TBT が悪化する、という一見わけの分からない結果になります。JavaScript は1行も触っていないのに、です。改善の報告をしている最中にこれが起きると、説明が面倒なことになります。「画像を軽くしました。ただし別の数値は悪化しています」では、相手は何が起きたのか分かりません。実際には悪化したのではなく、それまで測れていなかったものが測れるようになっただけなのですが、そう説明するには計測窓の話から始める必要があります。
では何を見ればいいか
TBT の代わりに、長時間タスクそのものを見ます。ブラウザのコンソールに貼れば、その場で一覧が取れます。同じところで止まった方がいるはずなので、実際に使っているものをそのまま置いておきます。
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(Math.round(entry.startTime), 'ms から', Math.round(entry.duration), 'ms');
}
}).observe({ type: 'longtask', buffered: true });
// buffered: true を付けないと、貼り付けた後のタスクしか拾えない
// startTime を見るのが要点。duration だけ見ても計測窓との関係が分からないstartTime を必ず一緒に出すのが要点です。所要時間だけを見ていると、それが計測窓の内側なのか外側なのか分かりません。FCP の値と並べて、はじめて TBT に乗るかどうかが判断できます。
Lighthouse のレポート側なら、診断セクションの「メインスレッドの長いタスクを回避してください」です。あちらは計測窓に関係なく、長時間タスクを列挙してくれます。
実務での読み方
ひとつだけ覚えて帰るなら、これです。FCP が3秒を超えているページの TBT は、そもそも読まない。
窓がどこから始まっているか分からない数値を眺めても、判断材料になりません。そういうページでは先に画像を直して FCP を下げる。TBT を見るのはそのあとです。順番が逆だと、数値が動いた理由を取り違えます。
残りは細かい話なので、並べるだけにします。
- TBT 0ms は「JavaScript に問題がない」の証明にはならない
- 画像と JavaScript は同時に直す。片方だけだと数値が素直に動かない
- 報告で TBT の改善幅だけを成果として出さない。計測窓が動いた分が混ざっている
Core Web Vitals の指標は、どれも定義が細かくできています。定義を読まないまま数値だけ比べると、逆の結論に着地することがある。TBT はそのなかでも、窓の位置が他の指標に依存している分、単独で読むと危ない指標だと思っています。