やあ。今日も今日とてコードレビューに追われているかい?
「なんだかこの画面、ボタンをクリックしてからモーダルが開くまで妙に引っかかるんだよね……」
パフォーマンス計測ツールを開いてみたら、綺麗に赤いロングタスクのバーがそびえ立っている。……実務でよくある光景だよね。
フロントエンドエンジニアとして中級からもう一段階上のレイヤーへ行くために、避けて通れないのが「ブラウザのメインスレッドとの付き合い方」だ。
今日は、JSの長大な処理(Long Task)がなぜ画面をカクつかせるのか、その裏側の仕組みを解剖し、`setTimeout`や`requestIdleCallback`を使ってどうスマートにタスクを分割するか、現場のリアルな知見を交えて解説しよう。
—
1. ブラウザの心臓部:メインスレッドの孤独と過酷さ
まず、WebブラウザのレンダリングエンジンとJavaScriptの実行環境(V8など)が、基本的に「単一のメインスレッド(Main Thread)」の上で同居しているという事実を思い出してほしい。これがすべての元凶であり、同時に美しくもある制約だ。
メインスレッドは、1つのレーンしか持たない超多忙なマルチタスクの職人だ。この職人は以下の仕事をたった1人で、しかも順番に(直列で)こなしている。
1. HTMLのパースとDOMツリーの構築
2. CSSのパースとCSSOMツリーの構築
3. JavaScriptの実行(イベントハンドラ、APIレスポンスの処理など)
4. スタイルの計算(Recalculate Style)
5. レイアウト/リフロー(Layout)
6. ペイント/描画(Paint / Composite)
ここで想像してほしい。JavaScriptで「1万件の配列をゴリゴリにループさせて複雑な計算とデータ加工を行う重い処理(Long Task)」をメインスレッドに投げたとしよう。
この職人は、そのJavaScriptの計算が終わるまで、他のすべての作業を完全にストップせざるを得ない。
ユーザーが画面をスクロールしようが、ボタンをクリックしようが、アニメーションのフレーム(1秒間に60回、つまり約16.6msごとに画面を更新するサイクル)を進めようが、職人は「ちょっと待て、今JavaScriptの計算で手一杯だ!」と無視し続ける。
結果として何が起きるか? 画面がピタッとフリーズする。これがメインスレッドのブロッキングだ。
—
2. 50msの壁と「レイフレーム」の正体
W3CやGoogleのWeb Vitalsの指標でもおなじみだが、ブラウザの世界では「50ms」がひとつの大きな分水嶺だ。
人間の脳は、操作に対する反応が100ms以内であれば「瞬間的(滑らか)」と感じる。しかし、ブラウザが60fps(16.6ms/フレーム)の滑らかさを維持しつつ、ユーザーの入力(INP: Interaction to Next Paint)に迅速に応答するためには、1つのタスクが50msを超えて居座ってはならない。
50msを超えるタスクは「Long Task」とみなされ、メインスレッドを占有し、レンダリングを遅延させ、ユーザー体験(UX)をドブに捨てることになる。
では、膨大なデータを処理しなければならないビジネスロジックがある時、僕たちはどうすればいいのか?
答えはシンプルだ。「仕事を細切れにして、ブラウザの隙間に差し込む(タスク分割:Task Splitting)」ことだ。
—
3. 実践!タスク分割の二大巨頭:`setTimeout` と `requestIdleCallback`
タスクを分割するアプローチとして、実務でよく使う2つの手法を解説しよう。
手法A: `setTimeout(…, 0)` によるマクロタスクの譲歩
一番古典的かつ確実なのが、お馴染みの `setTimeout` だ。
処理を小さなチャンク(塊)に切り分け、次のチャンクを実行する前に `setTimeout` でブラウザに一度制御を返す。これにより、ブラウザは「お、今のうちにレンダリングやユーザー入力を処理できるぞ」と息をつくことができる。
手法B: `requestIdleCallback` による「暇な時間」のハック
もっとモダンでスマートなアプローチが `requestIdleCallback` だ。
これは、「ブラウザがメインスレッドの仕事を一通り終えて、次のフレームの描画まで少し暇(アイドル状態)ができたら呼んでくれ」とブラウザにお願いするAPIだ。重要度の低いバックグラウンド処理(ログ送信、事前キャッシュ、重いデータの裏での整形など)に最適だ。
—
4. 現場で使える!タスク分割のきれいな実装サンプル
百聞は一見に如かず。実際に10,000件の重いデータを処理するモックを考えてみよう。
これを一気に処理するとメインスレッドが死ぬので、`async/await` と `setTimeout` を組み合わせたジェネレーター的アプローチ(あるいはチャンク分割)で処理を優しく刻むコードだ。
/
- 重い処理を一定のチャンク(塊)に分割して実行し、メインスレッドのブロッキングを防ぐユーティリティ
- @param {Array} items 処理対象の全データ
- @param {Function} processor 各アイテムを処理するコールバック
- @param {Number} chunkSize 1回あたりに処理する件数
/
async function processChunksInMainThread(items, processor, chunkSize = 500) {
let index = 0;
while (index < items.length) { const chunk = items.slice(index, index + chunkSize); // チャンク単位で処理を実行 for (const item of chunk) { processor(item); } index += chunkSize; // まだ処理が残っている場合、一度メインスレッドに制御を返す(yieldする) if (index < items.length) { await yieldToMainThread(); } } } /
- setTimeoutを使ってブラウザに描画やイベント処理の隙間(ブレイクタイム)を与える関数
/
function yieldToMainThread() {
return new Promise(resolve => {
// 0ms(実質最小遅延)でタスクキューの末尾にプッシュし、メインスレッドを解放する
setTimeout(resolve, 0);
});
}
// — 【使用例】 —
// 1万件の重いデータを模した配列
const hugeDataSet = Array.from({ length: 10000 }, (_, i) => ({ id: i, value: `data_${i}` }));
const results = [];
console.time(‘重い処理の実行時間’);
// メインスレッドをブロックせずに非同期でチャンク処理を回す
processChunksInMainThread(
hugeDataSet,
(item) => {
// 1件あたりの重い処理(例:複雑なパースや演算)
const computed = { …item, processedAt: performance.now() };
results.push(computed);
},
500 // 500件ずつ刻む
).then(() => {
console.timeEnd(‘重い処理の実行時間’);
console.log(‘すべての処理がスムーズに完了しました!UIは一切カクついていません。’, results.length);
});
このコードのミソは、`await yieldToMainThread()` の部分だ。
`setTimeout(resolve, 0)` を挟むことで、JavaScriptの実行コンテキストがいったん切れ、ブラウザは溜まっていた「再描画(Paint)」や「ユーザーのクリックイベント」をその隙間にねじ込むことができる。
ユーザーから見れば、「大量データ処理中も画面がスムーズにスクロールし、ボタンもサクサク反応する」理想的な体験になるわけだ。
—
5. チーフアーキテクトからの実践的なアドバイス
最後に、実務でこのテクニックを導入する際の注意点をいくつか授けておこう。
1. すべての処理を分割する必要はない
分割すること自体にも、タスクのスイッチングコスト(オーバーヘッド)がある。数件〜数十件程度の軽い処理にこれをやると、かえって全体が遅くなる。「50msを超える兆候がある重いループや再帰処理」にだけピンポイントで適用しよう。
2. Web Workerという究極の選択肢も忘れるな
もしその処理がDOMを一切触らない純粋な計算(データ構造の変換、暗号化、画像処理など)であれば、メインスレッドでタスク分割するよりも、Web Workerを使って別スレッド(別プロセス)に丸投げするのが現代のベストプラクティスだ。メインスレッドを完全に無傷の状態に保てる。
3. 必ず実機(あるいはCPUスロットリング環境)で計測しろ
ハイスペックな開発用MacBookで動かして「問題ない」と言い張るな。Chrome DevToolsの「Performance」タブを開き、CPUを「4x slowdown」や「6x slowdown」に throttling(低速化)した状態で、自分の書いたコードがLong Taskを発生させていないか必ず自分の目で確認するんだ。
フロントエンドのパフォーマンスチューニングは、ブラウザの気持ち(仕組み)をどれだけ想像できるかにかかっている。
「今、ブラウザのメインスレッドくんは忙しいから、ちょっと休憩を挟んであげよう」――この優しさが、プロのコードと素人のコードを分ける境界線だ。
さあ、今日のデプロイも、滑らかな画面で行こう!

コメント