世界を動かす技術を、日本語で。

ブラウザのメインスレッドは高コストである

2026年9月1日原文(kciter.so)

概要

  • フロントエンド最適化 はネットワークやバンドルサイズ削減だけでなく、 メインスレッド管理 が重要
  • メインスレッドのブロック が原因で、スクロールのカクつきや遅延入力が発生
  • 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 で次フレームの直前に再開、フレームサイクルと同期

まとめ

  • メインスレッドの適切な管理 こそが、現代フロントエンド最適化の要
  • 分割・バッチ化・優先順位付け・遅延 でメインスレッドの負荷分散を徹底
  • ユーザー体験を損なわないためには、 「一度に全てやらない」 設計が不可欠
  • パフォーマンス指標 を意識し、メインスレッドの使い方を最適化する姿勢が重要