📝 注意
本記事はAIの補助を受けて編集しています。
内容は大規模Webアプリケーションの実務経験に基づいています。
📚 目次
- 問題:「全部速いのに UI が重い」
- メインスレッド – ブラウザの「一方通行」道路
- メインスレッドを奪う3つの隠れた敵
- 実戦例:悪い例 → 良い例 → より良い例
- シニアエンジニアでも見落としがちな知見
- 毎日使えるメインスレッド対策チェックリスト
- 最終まとめ
1. 問題:「全部速いのに UI が重い」
こんな経験はありませんか?
- キーボードをカタカタ打つのに、文字がもっさり表示される
- スクロールがガクガクして、まるで古いゲームのよう
- アニメーションが一瞬止まってから再開する
DevTools で調べると:
- ネットワークは空いている
- FPS は 60 から 20 に落ちている
- メインスレッドが長時間占有されている(パフォーマンスタブの長い赤いバー)
皮肉な事実: 処理自体は 100ms しかかからなくても、その 100ms が連続してメインスレッドを離さないなら、ユーザーはラグを感じます。
パート5 では Event Loop の仕組み(タスクの待ち行列)を学びました。
パート6 では次の問いに答えます:
なぜ「短いタスク」でもフレームが落ちるのか? どうやってブラウザに「呼吸」させるのか?
2. メインスレッド – ブラウザの「一方通行」道路
メインスレッドは お祭り会場への狭い通路 のようなものです。全ての処理がここを通ります。
| 処理区分 | 具体例 |
|---|---|
| JavaScript | クリック処理、計算、API 呼び出し |
| Style | CSS セレクタの解決、スタイル計算 |
| Layout (reflow) | 各要素のサイズ・位置を確定 |
| Paint | ピクセルをレイヤーに描画 |
| Composite | レイヤーを合成して画面表示 |
不変のルール:
同時にできることは 1 つだけ。他の処理は待ち行列へ。
正常なフレームのタイムライン
Long Task によるフレーム破壊
JS が 50ms 以上占有 → Layout / Paint が待たされる → フレーム脱落。
3. メインスレッドを奪う3つの隠れた敵
3.1. Long Task – 長すぎる処理
50ms 以上 メインスレッドを占有するタスク。
原因:巨大ループ、巨大 JSON のパース、大量描画など。
// ❌ Long Task の典型例
function processBigArray(arr) {
for (let i = 0; i < arr.length; i++) {
doSomething(arr[i]); // 10万回ループ → 100ms
}
}
3.2. Layout Thrashing – レイアウト強制読み出し
スタイル変更の直後に offsetHeight / getBoundingClientRect() などのレイアウト情報を読み出すと、ブラウザはその場でレイアウトを強制計算します。
// ❌ Layout Thrashing
boxes.forEach(box => {
box.style.width = '100px'; // 書き込み
const height = box.offsetHeight; // 読み出し → レイアウト強制
box.style.height = height + 'px'; // さらに書き込み
});
読み出しのたびに 「ジャーク」 が発生し、スレッドが詰まります。
3.3. Render Storm – 再描画の嵐
短時間に状態変更が多すぎると、ブラウザはレイアウト+描画を連続して強制されます。
// ❌ 1万回の再描画 (React)
items.forEach(item => {
setItems(prev => [...prev, item]); // 毎回レンダリング
});
4. 実戦例:悪い例 → 良い例 → より良い例
4.1. スクロール時に大量データを処理する
シナリオ: スクロールのたびに5000件のデータを処理して UI 更新。
❌ 悪い例 – 完全ブロック
function onScroll() {
const results = heavyProcess(allItems); // 300ms
updateUI(results);
}
✅ 良い例 – setTimeout で譲る
function onScroll() {
setTimeout(() => {
const results = heavyProcess(allItems);
updateUI(results);
}, 0);
}
✅✅ より良い例 – requestIdleCallback でチャンク処理
function processChunk(items, index = 0) {
const chunk = items.slice(index, index + 100);
doWork(chunk);
if (index + 100 < items.length) {
requestIdleCallback(() => processChunk(items, index + 100));
} else {
updateUI();
}
}
チャンク処理の流れ
4.2. offsetHeight を読むとアニメーションがカクつく
シナリオ: マウスに追従するボックス。
❌ 悪い例
function moveBox(x, y) {
box.style.transform = `translate(${x}px, ${y}px)`;
const rect = box.getBoundingClientRect(); // レイアウト強制
console.log(rect);
}
✅ 良い例 – レイアウト読み出しを避ける
function moveBox(x, y) {
box.style.transform = `translate(${x}px, ${y}px)`;
// レイアウト情報は読まない(または後で読む)
}
4.3. scroll イベント – フレームドロップの殺し屋
❌ 悪い例 – 毎回重い処理
window.addEventListener('scroll', () => {
updateHeavyWidget();
});
✅ 良い例 – requestAnimationFrame + スロットリング
let ticking = false;
window.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(() => {
updateHeavyWidget();
ticking = false;
});
ticking = true;
}
});
5. シニアエンジニアでも見落としがちな知見
50ms ルールと Long Task API
Google の RAIL モデル より:
全てのタスクは 50ms 以内 に収めること。超えるとユーザーは遅延を感じる。
ブラウザは Long Task を検出する API を提供しています。
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.warn(`[LONG TASK] ${entry.duration}ms - ${entry.attribution?.[0]?.containerName || 'unknown'}`);
}
});
observer.observe({ entryTypes: ['longtask'] });
これを実際のアプリで実行すると、多くの驚きがあるはずです。
パフォーマンス ≒ 速度ではない
100万件の合計を 10ms で計算できても、その10msがスクロール中に発生すれば、あなたはフレームを落としました。
正しい考え方:
パフォーマンスとは ユーザー体験を損なわずに処理を完了する能力 である。
6. 毎日使えるメインスレッド対策チェックリスト
コードレビューや自己チェックにどうぞ。
✅ JavaScript
- 1000件以上の同期的ループがない(チャンク処理を使う)
- 50ms 超えるタスクは細分化 or Web Worker
-
バックグラウンド処理には
requestIdleCallback - Long Task API で定期的に監視
✅ Rendering
- スタイル変更の直後にレイアウト読み出しをしない(Layout Thrashing 禁止)
- DOM 更新はバッチ処理(DocumentFragment や仮想 DOM を活用)
-
不要な再描画を避ける(React:
memo,useMemo) -
アニメーションは
top/leftではなくtransformを使う
✅ イベント
-
scroll/resize/mousemoveは適切に間引き(debounce/throttle) - イベントハンドラ内で重い処理を直接呼ばない
-
アニメーションや描画変更は
requestAnimationFrame内で
✅ アーキテクチャ
- 長いリストは仮想化(react-window / vue-virtual-scroller)
- コード分割 – 初回ロードで全部読み込まない
- 重要でないコンポーネントは遅延ロード
7. 最終まとめ
パート5 では Event Loop(タスクの待ち行列)を学びました。
パート6 では タスクがメインスレッドを占有し続けると何が起こるか を掘り下げました。
✨ 成功の鍵:
メインスレッドを ブラウザで最も貴重なリソース として扱いましょう。
決して「マラソン」させてはいけません。短いスプリントと休憩を繰り返させるのです。
👉 次回予告
[Frontend Performance - Part 7] Long Task を分解する:なぜ50msがUXを壊すのか?
