【実務・中級編】 リフロー(再レイアウト)を発生させるプロパティ一覧 – Webブラウザの仕組み実践ガイド

やあ。最近、プロダクトのパフォーマンス改善に頭を悩ませているようだね。よし、今日はフロントエンドエンジニアなら誰もが一度はハマる、そして避けて通れない「リフロー(再レイアウト)」の魔物について話をしよう。

「なぜかスクロールがカクつく」「アニメーションがバターのように滑らかに動かない」。
その原因、大抵こいつ(強制同期レイアウト)が裏で悪さをしているせいだ。

今日は、ブラウザの内部で何が起きているのかという根本的な仕組みから、知らず知らずのうちに地雷を踏み抜くヤバいプロパティの網羅、そしてそれを華麗に回避するための実践的なテクニックまで、シニアの視点でみっちり叩き込んでやる。心して聞いてくれ。

—

1. ブラウザの裏側で何が起きているのか?DOMからレイアウトまでの泥臭い真実

まず、ブラウザがHTMLを読み込んでから画面にピクセルを描画するまでのプロセスをおさらいしよう。お前も知っている通り、大まかな流れはこうだ。

1. HTMLのパース & DOMツリー構築
2. CSSのパース & CSSOMツリー構築
3. レンダリングツリー(レイアウトツリー)の構築
4. レイアウト(リフロー / 再計算)
5. ペイント(描画)

問題なのは、4番目の「レイアウト(リフロー)」だ。
ブラウザは、各要素が画面上のどこに、どれくらいのサイズで配置されるかを計算しなければならない。要素の幅、高さ、絶対位置などが変われば、当然その周りの兄弟要素や親・子要素のレイアウトも連鎖的に再計算する必要がある。これが「リフロー」だ。

そして、最も恐ろしいのが 「強制同期レイアウト(Forced Synchronous Layout / Layout Thrashing)」 という現象だ。

レイアウト・スラッシングの悪夢

通常、ブラウザはパフォーマンスを最適化するために、JavaScriptからのDOM変更をキューに溜め込み、イベントループの適切なタイミング(通常は毎フレームの描画前)で一気にレイアウトを計算する。

しかし、「JavaScriptでDOMのスタイルを書き換えた直後に、その要素のレイアウト情報(位置やサイズ)を読み取ろうとした瞬間」、ブラウザはこう判断する。
「おい、さっき変更したスタイルのせいで最新のレイアウトが確定してないぞ。でも、こいつ今すぐ正確な座標を知りたがってる……仕方ねぇ、他の処理をすべて止めて、今すぐレイアウトを強制的に再計算(リフロー)するか!」

これが強制同期レイアウトだ。これを1つのフレーム内で何度も繰り返す(書き込み→読み取り→書き込み→読み取り……)と、ブラウザのメインスレッドは完全にパンクする。これが「レイアウト・スラッシング」の正体だ。

—

2. 【実録】アクセスしただけでリフローを引き起こす「危険なプロパティ」たち

では、具体的にどんなプロパティやメソッドがブラウザに「今すぐリフローしろ!」と命令してしまうのか。これらはJavaScriptから「読み取る(getter)」瞬間に発動する。

ジオメトリ(位置・サイズ)に関するプロパティ

これらの値をJS側で取得しようとすると、ブラウザは強制的にレイアウトを走らせる。

  • `element.offsetLeft`, `element.offsetTop`, `element.offsetWidth`, `element.offsetHeight`
  • `element.clientLeft`, `element.clientTop`, `element.clientWidth`, `element.clientHeight`
  • `element.getBoundingClientRect()`
  • `element.scrollWidth`, `element.scrollHeight`, `element.scrollTop`, `element.scrollLeft`

スタイル・算出スタイルに関するプロパティ

  • `window.getComputedStyle(element)` (※厳密には、これに含まれるレイアウト関連のプロパティを読み取る際や、直前にDOM変更がある場合)
  • `element.style.width` などの「書き込み」の直後の「読み取り」

特に、`getBoundingClientRect()` は要素のバウンディングボックスを一発で取得できるので重宝しがちだが、ループの中でこいつを呼び出そうものなら、パフォーマンス測定ツール(LighthouseやPerformanceタブ)で真っ赤な警告(Long Tasks)が出るお墨付きの劇薬だ。

—

3. 現場で使える!リフローを回避・最小化するベストプラクティス

じゃあ、DOMの位置やサイズを測りながらインタラクティブなUIを作るのは諦めなきゃいけないのか? そんなわけない。プロなら、ブラウザを騙し、手玉に取るスマートなやり方がある。

黄金律 1: 「読み取り」と「書き込み」を完全に分離する(バッチ処理)

レイアウト・スラッシングを防ぐ基本中の基本は、「DOMの書き込みは書き込みだけでまとめて行い、読み取りは読み取りだけでまとめて行う」ことだ。ごちゃ混ぜにするからブラウザが混乱する。

❌ やってはいけないアンチパターン(レイアウト・スラッシングの温床)

// ループの中で「書き込み」と「読み取り」を交互に行っている
const boxes = document.querySelectorAll(‘.box’);

for (let i = 0; i < boxes.length; i++) { // 【書き込み】スタイルの変更 boxes[i].style.width = (boxes[i].offsetWidth + 10) + 'px'; // ↑ ここで offsetWidth を「読み取る」ために、毎回強制リフローが発生する!地獄絵図。 }

⭕ 正しいアプローチ(バッチ処理による最適化)

const boxes = document.querySelectorAll(‘.box’);

// 1. まず「読み取り」をすべて終わらせてメモリにキャッシュする
const widths = Array.from(boxes).map(box => box.offsetWidth);

// 2. その後で「書き込み」をまとめて実行する
for (let i = 0; i < boxes.length; i++) { boxes[i].style.width = (widths[i] + 10) + 'px'; // これなら、ブラウザのリフローは最後に一回だけで済む。 }

黄金律 2: `requestAnimationFrame` を使ってブラウザの描画サイクルに合わせる

アニメーションやスクロールイベントなどでDOMをいじる際は、ブラウザのレンダリングサイクル(通常60fpsなら約16.6msごと)のタイミングに処理を同期させるのが鉄則だ。

let isTicking = false;

function onScroll() {
// スクロール位置の「読み取り」
const scrollTop = window.pageYOffset;

if (!isTicking) {
window.requestAnimationFrame(() => {
// 次の描画フレームの直前に「書き込み」を実行する
document.querySelector(‘.header’).style.transform = `translateY(${scrollTop 0.5}px)`;
isTicking = false;
});
isTicking = true;
}
}

window.addEventListener(‘scroll’, onScroll, { passive: true });

※ `{ passive: true }` をつけることで、ブラウザに対して「このイベントリスナーは `preventDefault()` を呼び出しませんよ」と宣言し、スクロールの滑らかさを担保するのもシニアとしての基本マナーだ。

黄金律 3: リフローを「レイアウト不要なプロパティ(Composite)」に逃げる

そもそも、要素の `width` や `height`、`top` や `left` をアニメーションさせると、毎回リフロー(またはリペイント)が発生してCPUが悲鳴を上げる。

モダンブラウザの仕組みを思い出してほしい。GPU(コンポジター)の力を借りて処理できるプロパティ、つまり `transform` (translate, scale など) や `opacity` を使えば、リフローもリペイントもバイパスして、GPUによる合成(Composite)だけでシルキーなアニメーションを実現できる。

  • ❌ `element.style.left = ‘100px’` (リフローが発生!)
  • ⭕ `element.style.transform = ‘translateX(100px)’` (GPU処理・リフローなし!)

レイアウトを変える必要がある場合を除き、視覚的な移動や拡大縮小は `transform` に逃げるのが現代フロントエンドの絶対正義だ。

—

4. まとめ:明日からお前のコードで実践すべきこと

長々と語ってきたが、要するに意識すべきことはシンプルだ。

1. 「DOMの読み取り」と「書き込み」を同じループ内で混ぜるな。
2. 位置やサイズを測るプロパティ(`offsetWidth` や `getBoundingClientRect` など)のコストの高さを自覚しろ。
3. アニメーションや動的なスタイル変更は、極力 `transform` や `opacity` を使ってレイフローを回避しろ。

ブラウザの内部挙動を想像できるようになると、フロントエンドを書くときの「手の感覚」が変わってくる。「あ、今ここでこのプロパティを呼んだらブラウザ裏で再計算走っちゃうな」とセンサーが働くようになるはずだ。

さあ、理屈は分かったな。今すぐ自分のプロジェクトのパフォーマンスタブを開いて、レイアウト・スラッシングの赤い波形をキレイに消し去ってこい!健闘を祈る。

コメント

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