【実務・中級編】 Total Blocking Time (TBT) の理解と最適化 – Webブラウザの仕組み実践ガイド

メインスレッドの「沈黙」を奪還せよ:TBTを極めるフロントエンドの極意

やあ。今日も一日中、ChromeのDevToolsと睨めっこしていたんじゃないか?

Webパフォーマンスの世界で、「Lighthouseでスコアを叩き出せ」と言われて真っ先にやるのが画像の最適化やフォントの調整だ。だが、どれだけ画像を軽くしても、Total Blocking Time (TBT) が高いままだと、ユーザーは「動くのに触れない」という地獄のようなUXを味わうことになる。

今日は、ブラウザの心臓部である「メインスレッド」をいかに解放し、ユーザーが触れる状態(TTI)を早めるか。その泥臭い現実と、明日から使える戦術を共有しよう。

—

1. なぜ「メインスレッドの沈黙」が許されないのか

ブラウザのメインスレッドは、いわば「何でも屋の支配人」だ。HTMLのパース、JSの実行、スタイルの計算、リフロー、リペイント……これらを全て一人でこなしている。

TBTは、「First Contentful Paint (FCP) から Time to Interactive (TTI) の間」に発生する、50msを超えるロングタスクの合計時間だ。

ブラウザの仕様上、メインスレッドがJavaScriptの重い処理で埋まっている間、ユーザーのクリックやスクロールイベントは「行列に並ばされる」ことになる。50msを超えた瞬間、ブラウザは「おい、そろそろユーザーがイライラし始めるぞ」と判断し、そのタスクを「ロングタスク」としてカウントする。これがTBTの正体だ。

2. 実践的最適化:タスクを「分割」して支配人を休ませろ

メインスレッドを解放する唯一にして最大の原則は、「重い処理を細切れにして、隙間にブラウザのタスクをねじ込む」ことだ。

悪い例:メインスレッドを殺す「一括処理」

例えば、大量のデータをループで処理してDOMを更新するようなコードは、メインスレッドを数秒間完全に占拠してしまう。

// 絶対にやってはいけない:メインスレッドを独占する処理
function processMassiveData(data) {
// ループが長すぎると、ブラウザは再描画や入力を一切受け付けない
data.forEach(item => {
updateDOM(item); // ここでリフロー/リペイントが発生するとさらに地獄
});
}

良い例:`scheduler.yield` または `setTimeout` を使った分割

ブラウザが次のタスクを処理できるように、意図的に「休憩」を挟むんだ。

// 推奨:タスクを小分けにしてメインスレッドを解放するパターン
async function processDataInChunks(data, chunkSize = 50) {
for (let i = 0; i < data.length; i += chunkSize) { const chunk = data.slice(i, i + chunkSize); // 現在の処理を一旦中断し、ブラウザに制御を返す // 0msでも、ブラウザは「あ、今なら入力受け付けられるな」と判断して隙間を処理してくれる await new Promise(resolve => setTimeout(resolve, 0));

chunk.forEach(item => {
// DOM操作はなるべく一括で行うか、DocumentFragmentを使うのが吉
processItem(item);
});
}
}

3. 「リフロー」という名の爆弾を封印せよ

TBTを押し上げるもう一つの大きな要因は、JS実行中に発生する「強制同期レイアウト(Layout Thrashing)」だ。

// 最悪のパターン
const elements = document.querySelectorAll(‘.item’);
elements.forEach(el => {
// スタイルを読み取る(ブラウザはリフローを強制される)
const height = el.offsetHeight;
// スタイルを書き込む(またリフローが起きる)
el.style.height = (height + 10) + ‘px’;
});

DOMを読み取ってから書く処理を繰り返すと、ブラウザは「DOMが書き換わったから再計算しなきゃ」と、何度も律儀にリフローを実行する。これを防ぐには、「読み取りフェーズ」と「書き込みフェーズ」を物理的に分けること。これだけで、メインスレッドの負荷は劇的に軽くなる。

4. 現場のシニアが教える「隠し球」Tips

1. Reactを使っているなら `useDeferredValue` と `useTransition` を使いこなせ:
これらは内部的にタスクの優先度を下げ、メインスレッドが空いている時にレンダリングを差し込む仕組みだ。フレームワークの恩恵を最大限に活用しよう。
2. Web Workerへの逃避:
どうしても重い計算(JSONのパース、複雑なフィルタリングなど)が必要なら、メインスレッドから叩き出せ。`new Worker()` を使って、支配人の仕事を部下に押し付けるんだ。
3. Lighthouseで「Long Tasks」を可視化せよ:
Chromeの「Performance」タブで、赤いバーが表示されている箇所がTBTの犯人だ。そこを拡大して、どのJS関数が実行されているかを特定する。「計測できないものは改善できない」。これは鉄則だ。

最後に:完璧を目指すな、ユーザー体験を目指せ

TBTを0にすることは不可能だ。ブラウザは動いている以上、何かしらの計算は必要だからな。
だが、「ユーザーが何かをしようとした瞬間に、ブラウザがフリーズしていないこと」。これさえ守れれば、Webアプリは魔法のようにサクサク動く。

まずは今のプロジェクトの「一番重い処理」を一つ選んで、`setTimeout` で分割してみろ。その小さな一歩が、ユーザーのストレスを劇的に減らすはずだ。

何か詰まったら、いつでも聞いてくれ。現場からは以上だ。頑張れよ!

コメント

タイトルとURLをコピーしました