【実務・中級編】 レイアウトスラッシングの回避パターン – Webブラウザの仕組み実践ガイド

ブラウザの悲鳴を止めろ:レイアウトスラッシングを撲滅する「読み書き分離」の哲学

やあ。現場でコードを書いていると、ふと「なぜか画面の描画がカクつく」「複雑なUIを作るとスクロールが重くなる」という現象に突き当たることがあるだろう。

その原因、十中八九「レイアウトスラッシング(Layout Thrashing)」だ。

ブラウザのレンダリングエンジンは、君たちが書いたコードを解釈して画面にピクセルを描き出すために、とてつもない労力を払っている。今日は、そのブラウザの裏側の泥臭い処理を理解し、お世辞にも効率的とは言えない「悪しきコード」をどう修正すべきか、その処方箋を授けよう。

—

なぜブラウザは「強制同期レイアウト」に苦しむのか

ブラウザは通常、レンダリングの効率を上げるために「バッチ処理」を行う。DOMの変更(書き込み)を溜め込んでおき、ブラウザが「よし、ここでまとめてレイアウトを計算するか」と判断したタイミング(フレームの切り替わり)で一気に計算するんだ。

しかし、君たちが「DOMを書き換えた直後に、その要素のプロパティを読み取る」というコードを書くとどうなるか。

1. 書き込み: `element.style.width = ‘100px’`
2. 読み取り: `const width = element.offsetWidth` (← ここで事件発生!)

ブラウザはこう思う。「さっき変更を加えたばかりだから、今のレイアウト情報は古い。最新の正確な数値を返すためには、今すぐレイアウト計算(リフロー)を完了させないと!」

これが強制同期レイアウト(Forced Synchronous Layout)だ。ブラウザは本来のスケジュールを無視して、その瞬間に計算をやり直す。これをループの中で何度も繰り返せば、レンダリングエンジンは悲鳴を上げる。これがレイアウトスラッシングの正体さ。

—

「読み」と「書き」を切り離す:ベストプラクティス

解決策はシンプルだ。「DOMへの書き込み」と「DOMからの読み取り」を分離し、一箇所に集約する。 これに尽きる。

以下に、アンチパターンと、それを現場でどう修正すべきかの対比コードを置いておく。

【アンチパターン】ループ内で読み書きを繰り返す(死のループ)

// これは最悪だ。要素の数だけレイアウト計算が走る。
const items = document.querySelectorAll(‘.list-item’);
items.forEach(item => {
// 書き込み
item.style.width = ‘200px’;
// 読み取り(ここで強制的にレイアウト計算が発生!)
console.log(item.offsetWidth);
});

【ベストプラクティス】読みと書きを完全に分離する

// 書き込みをバッチ化し、読み取りも一箇所にまとめる
const items = document.querySelectorAll(‘.list-item’);

// 1. まず「読み取り」を完了させる
const widths = Array.from(items).map(item => item.offsetWidth);

// 2. 次に「書き込み」をまとめて行う
items.forEach((item, index) => {
item.style.width = (widths[index] + 10) + ‘px’;
});

—

実務で使える「FastDOM」的な思考

もし君のアプリケーションが非常に複雑で、複数のコンポーネントが勝手にDOMを触るような環境なら、自前で「キュー(待ち行列)」を作るのも一つの手だ。

/

  • シンプルな読み書き分離のラッパー
  • 実際の現場ではこのようなユーティリティを介してDOM操作を管理する

/
const DOMTaskQueue = {
reads: [],
writes: [],

measure(task) { this.reads.push(task); },
mutate(task) { this.writes.push(task); },

flush() {
// 読み取りを先に処理
this.reads.forEach(t => t());
// 書き込みを後に処理
this.writes.forEach(t => t());

// キューをクリア
this.reads = [];
this.writes = [];
}
};

// 使用例
DOMTaskQueue.measure(() => console.log(el.offsetWidth));
DOMTaskQueue.mutate(() => el.style.width = ‘100px’);

// フレームの最後(requestAnimationFrame)で実行するのがコツだ
requestAnimationFrame(() => DOMTaskQueue.flush());

—

最後に:ブラウザと仲良くするために

レイアウトスラッシングを避けることは、単なるパフォーマンスチューニングじゃない。それは、ブラウザという「限られたリソースで最高の体験を提供しようとする健気なマシン」に対する、エンジニアとしての礼儀なんだ。

もし自分の書いたコードが重いと感じたら、Chrome DevToolsの「Performance」タブを開いてみてほしい。「紫色のバー(Recalculate Style)」や「橙色のバー(Layout)」が細かく刻まれていたら、それは君のコードがブラウザに無理強いをしているサインだ。

「読み込み」と「書き込み」を分ける。たったこれだけの習慣で、君のUIは驚くほど滑らかになるはずだ。

現場からは以上だ。また何かあればいつでも聞いてくれ。

コメント

タイトルとURLをコピーしました