レイアウト・スラッシングを葬り去れ:ブラウザの悲鳴を聞き分ける技術
フロントエンドの現場で「なんかスクロールがカクつく」「アニメーションが妙に重い」という相談を受けるたび、私はまずデベロッパーツールの「Performance」タブを開いて、真っ赤に染まった「Layout」の山を確認する。
心当たりのあるエンジニアも多いだろう。そう、Layout Thrashing(強制同期レイアウト)だ。今回は、ブラウザが裏側でどれほど悲鳴を上げているのか、そしてそれをどうスマートに解決すべきか、プロの視点で徹底的に解剖しよう。
—
1. なぜブラウザは「キレる」のか?
ブラウザのレンダリングパイプラインを簡潔に言えば、「DOM/CSSOM」→「スタイル計算」→「レイアウト(配置)」→「ペイント(描画)」→「コンポジット(合成)」という流れだ。
通常、ブラウザは「このJSが終わるまで待てば、まとめてレイアウト計算してやるか」という賢い最適化を行っている。これを「遅延レイアウト」と呼ぶ。
しかし、我々がJSでこんなコードを書くと状況が一変する。
// 【アンチパターン】レイアウト・スラッシングの典型例
const box = document.getElementById(‘box’);
for (let i = 0; i < 100; i++) { // ①書き込み:スタイルを変更(レイアウトが無効化される) box.style.width = `${i}px`; // ②読み取り:offsetWidthを要求(ブラウザは強制的にレイアウトを再計算するしかない) console.log(box.offsetWidth); } このコードを実行すると、ブラウザは「えっ、まだJS書いてる途中だろ!? でも値が必要なら今すぐ計算しろってことか…」と、ループの回数分だけ強制的にレイアウト計算を走らせる。 これがLayout Thrashingだ。ブラウザにとって、これはCPUをドブに捨てるような行為であり、ユーザーにはカクつきとして伝わる。
2. ブラウザの心臓部:強制同期レイアウトのメカニズム
ブラウザが `offsetWidth` や `getBoundingClientRect()` を呼び出された瞬間、何が起きているか想像してほしい。
1. DOMの変更: JSがDOMをいじった時点で、ブラウザは「レイアウトは汚れた(Dirty)」とフラグを立てる。
2. 強制同期: JSが「位置を教えろ!」と叫ぶ。ブラウザはDirtyフラグを見て、「くそっ、今すぐ計算しないと正確な値が返せない!」と、進行中のJSを停止させ、スタイル計算とレイアウト計算を同期的に実行する。
3. コストの重複: これをループ内で行うと、本来なら一度で済む計算が100回繰り返される。
これが「ブラウザをいじめる」という行為の正体だ。
—
3. 実践:スラッシングを回避する「読み書き分離」戦略
解決策はシンプルだ。「読み取り(Read)」と「書き込み(Write)」を混ぜるな。
書き込みは書き込みでまとめて、読み取りは読み取りでまとめて行う。これだけでブラウザの負担は激減する。
Before: 泥沼のコード
// 読み取りと書き込みが混ざっているため、ループのたびに再計算が発生
items.forEach(item => {
const newHeight = item.offsetHeight + 10; // 読み取り
item.style.height = `${newHeight}px`; // 書き込み
});
After: プロの最適化コード
// ① まずは全ての「読み取り」を先に済ませる
const heights = items.map(item => item.offsetHeight);
// ② 次に全ての「書き込み」を一気に実行する
items.forEach((item, index) => {
item.style.height = `${heights[index] + 10}px`;
});
このように、メモリ(配列など)に一時的に値を退避させ、DOMへのアクセスを最小限にする。これだけで、ブラウザは「おっ、一気に書き込んでくれるのか。じゃあレイアウト計算は1回でいいな」と判断してくれるわけだ。
—
4. さらに上を目指す:FastDOMの精神と`requestAnimationFrame`
現場で大規模なコンポーネントを扱うなら、毎回手動で管理するのは限界がある。そんな時は、`requestAnimationFrame (rAF)` を使って、ブラウザの描画サイクルに合わせるのが定石だ。
/
- 読み書きを分離して実行するユーティリティの簡易実装
/
const readWriteBatch = {
queue: [],
schedule(task, type) {
this.queue.push({ task, type });
requestAnimationFrame(() => {
// 読み取りフェーズ(先に全部やる)
this.queue.filter(q => q.type === ‘read’).forEach(q => q.task());
// 書き込みフェーズ(後に全部やる)
this.queue.filter(q => q.type === ‘write’).forEach(q => q.task());
this.queue = [];
});
}
};
// 使い方
readWriteBatch.schedule(() => console.log(el.offsetWidth), ‘read’);
readWriteBatch.schedule(() => el.style.width = ‘100px’, ‘write’);
※実際の実務では、[FastDOM](https://github.com/wilsonpage/fastdom) のようなライブラリを利用するか、Reactなどのフレームワークが内部で行っている最適化の恩恵を理解した上で、あえて生のDOMを触るべきか判断するのがプロの流儀だ。
—
最後に:エンジニアとしての矜持
Layout Thrashingを避ける技術は、単なるコードの書き方ではない。「ブラウザというプラットフォームと対話する」という姿勢そのものだ。
「なぜ今、ブラウザは重いのか?」「今、DOMツリーはどういう状態なのか?」を想像できるエンジニアこそが、ユーザーに最高の体験を提供できる。
公式マニュアルを暗記するだけでは辿り着けない、この「ブラウザの呼吸を感じる」感覚をぜひ日々の開発で意識してみてほしい。それができれば、君が書くコードは確実に、以前よりも滑らかに動くはずだ。

コメント