概要
- フロントエンド最適化 はネットワークやバンドルサイズ削減だけでなく、 メインスレッド管理 が重要
- メインスレッドのブロック が原因で、スクロールのカクつきや遅延入力が発生
- JavaScript実行 と 画面描画 のほとんどがメインスレッドで処理
- 長いタスク がメインスレッドを占有すると、ユーザー操作やアニメーションが停止
- 分割やバッチ化 などでメインスレッドの負担を分散し、パフォーマンス向上を図る
フロントエンド最適化の新たな視点:メインスレッドの重要性
- フロントエンド最適化 と聞くと、ネットワークリクエスト削減やバンドル縮小、キャッシュ活用などが一般的な発想
- しかし、 メインスレッドのブロック が発生すると、どれだけ最適化しても画面がフリーズ
- 特に インタラクションが多い画面 や ライブデータ更新、スクロール・アニメーション・入力が重なる場面で顕著
- スクロールのカクつき や ボタンの遅延反応、入力遅延など、微妙なストレスの原因
- コード自体の速度よりも、「どのコードがメインスレッドを占有しているか」が問題
メインスレッドの役割と仕組み
- ブラウザには複数スレッドが存在するが、 JavaScript実行・描画・イベント処理 など主要処理はメインスレッドに集中
- メインスレッドの主な仕事は JavaScriptの実行 と 画面描画パイプライン
- JavaScript実行:イベントハンドラ、タイマー、ネットワークコールバック、フレームワーク内部処理など
- 画面描画:requestAnimationFrameコールバック、スタイル計算、レイアウト(リフロー)、ペイント
- 描画パイプライン の大部分もメインスレッドが担当、最終的なコンポジットのみ専用スレッドに移譲
- 画面のスムーズさ維持には 1フレーム約16.6ms(60Hz) 以内の処理が必要(実質10ms程度が目安)
- JavaScriptはシングルスレッドイベントループ で動作し、1タスク中は他処理が進行不可
- 50ms超のタスク=ロングタスク はパフォーマンス問題の指標
メインスレッドブロックの体感とパフォーマンス指標
- ロングタスク発生時 は画面更新や入力受付が停止し、ユーザー体験に直結
- パフォーマンス指標として INP(Interaction to Next Paint) や TBT(Total Blocking Time) が存在
- これらは「どれだけメインスレッドがブロックされたか」を数値化
メインスレッド時間の使い方:4つの基本戦略
- 分割(splitting) :長い処理を小分けにし、間で制御を返す
- バッチ化(batching) :頻繁な処理をまとめて実行
- 優先順位付け(prioritizing) :複数タスクの実行順序を最適化
- 遅延実行(deferring) :今すぐ不要な処理を後回し
- まずは 分割 が基礎。分割があってこそバッチ化や優先順位付け、遅延が可能
分割の実践例:ライブチャットの描画
- ライブ配信のチャットでは 大量メッセージが一度に到着 しがち
- 一気にDOM描画すると、 入力やアニメーションが全て停止
- 20件ごとなどに分割し、setTimeoutで制御を返す ことで、間に描画や入力処理が挟まる
- 分割によって「仕事の合間」にブラウザが他の処理を進められるように
- setTimeout は次のタスクに処理を回すクラシックな方法
// チャットメッセージを分割描画
async function renderChats(chats) {
let count = 0;
for (const chat of chats) {
appendChatNode(chat);
if (++count % 20 === 0) {
await new Promise((resolve) => setTimeout(resolve, 0));
}
}
}
- 分割しても総作業量は変わらず、全体の処理時間はむしろ増加
- しかし ユーザー体感のスムーズさが大幅改善。描画や入力が割り込めるため
時間基準での分割:アニメーションとの協調
- アニメーションやスクロール中は 件数ではなく経過時間で分割 する方が安全
- requestAnimationFrame を使い、フレームごとに少しずつ処理を進める
- こうすることで アニメーションと重い処理が同時進行 し、どちらも滑らかに
async function processDuringAnimation(items) {
let i = 0;
let frameStart = performance.now();
while (i < items.length) {
while (i < items.length && performance.now() - frameStart < 5) {
doWork(items[i++]);
}
frameStart = await new Promise(requestAnimationFrame);
}
}
- performance.now() で経過時間を計測し、 5ms などの予算を守る
- requestAnimationFrame で次フレームの直前に再開、フレームサイクルと同期
まとめ
- メインスレッドの適切な管理 こそが、現代フロントエンド最適化の要
- 分割・バッチ化・優先順位付け・遅延 でメインスレッドの負荷分散を徹底
- ユーザー体験を損なわないためには、 「一度に全てやらない」 設計が不可欠
- パフォーマンス指標 を意識し、メインスレッドの使い方を最適化する姿勢が重要