お疲れ様です。最近、社内のプロダクトで「なんかこのページ、スクロールがカクつくんだよね」って言われて、DevToolsのPerformanceタブを開いたら真っ赤なFlame Chartに絶望した……なんて経験、ありませんか?
フロントエンドエンジニアとして中級の壁を突破し、シニアへとステップアップしていく過程で避けて通れないのが、この「ブラウザのレンダリングの裏側、特にレイアウト(Reflow)の最適化」です。
今回は、ブラウザが裏側でどう汗をかいて画面を描画しているのか、そして私たちが書くコードがいかにその負荷に直結しているのかを、現場のリアルな視点から徹底的に解剖していきます。
—
1. レイアウト(Reflow)とは何か?ブラウザの裏側で起きている悲劇
私たちが普段何気なく書いているJavaScriptやCSS。ブラウザはこれを受け取ると、DOMツリーとCSSOMツリーを合成し、最終的な画面を描画するための「レンダリングツリー」を作ります。
ここで重要なのは、「DOMやCSSOMが変わるたびに、ブラウザは再計算を強いられる」という事実です。
レイアウト(Reflow / Layout)の正体
レイアウトとは、レンダリングツリーのノードが「画面上のどこに、どのくらいのサイズで配置されるべきか」を幾何学的に計算するプロセスです。
親要素のサイズが変われば、子要素のサイズも連動して変わり、さらにその兄弟要素や祖先要素の位置までズレていく……。DOMツリー全体、あるいはその一部にドミノ倒しのように影響が波及します。これが、通称「リフロー(Reflow)」と呼ばれる重い処理です。
レイアウトの次にある「ペイント」と「合成」
ブラウザのレンダリングパイプラインは、大まかに以下のステップで進みます。
1. JS / CSSの変更:DOMの書き換えやクラスの付与
2. Recalculate Style(スタイル計算):どの要素にどのCSSが当たるかの再計算
3. Layout / Reflow(レイアウト):要素のジオメトリ(位置・サイズ)の計算 ← 今回の主役!
4. Paint(ペイント):ピクセルを画面に塗る処理(文字を描く、背景色を塗るなど)
5. Composite(合成):レイヤーを重ね合わせて画面を完成させる
この中で、LayoutとPaintはCPUを激しく消耗します。特にLayoutは、DOM構造が複雑であればあるほど計算量が爆発的に増えます。これが、60fps(1フレームあたり約16.6ms)の滑らかなUIを維持するための最大の障壁なんです。
—
2. 【最悪のアンチパターン】レイアウトスラッシング(Layout Thrashing)
現場で最もやりがちな、そして最もパフォーマンスを殺す最悪のバグが「レイアウトスラッシング(Layout Thrashing)」です。
ブラウザには「レンダリングの最適化機構(キューイング)」があります。例えば、JSでDOMの幅を書き換えるコードが連続しても、ブラウザは「どうせ最後にまとめて計算すればいいや」と賢く遅延評価(バッチ処理)してくれます。
しかし、「スタイルの書き換え」の直後に「レイアウトを伴うプロパティの読み取り」を行うと、ブラウザはこの最適化を強制解除せざるを得なくなります。 「今すぐ位置を教えろ」とJSが要求するため、ブラウザは仕方なく、その場で未完了のレイアウト計算を強制実行(強制同期レイアウト / Forced Synchronous Layout)させられます。これをループ内で何回もやるとどうなるか……想像しただけで冷や汗が出ますよね。
🚨 やってはいけないアンチパターンの例
// 【最悪の例】レイアウトスラッシングを引き起こすコード
const boxes = document.querySelectorAll(‘.box’);
// ループ内で「書き込み」と「読み取り」を交互に行っている!
boxes.forEach(box => {
// 1. 書き込み(スタイル変更:リフローの予約)
box.style.width = box.offsetWidth + 10 + ‘px’;
// 2. 読み取り(レイアウトの強制同期!ここでブラウザが止まる)
console.log(box.offsetHeight);
});
このコードを実行すると、ブラウザは「書き込み→強制計算→読み取り→書き込み→強制計算→読み取り……」という地獄のループを毎フレーム強制させられ、メインスレッドが完全にフリーズします。これがレイアウトスラッシングの正体です。
—
3. 回避策とベストプラクティス:実務で使えるテクニック
では、どうすればこのリフロー地獄から抜け出せるのでしょうか? 答えはシンプルです。「読み取りと書き込みを完全に分離(バッチ処理)する」ことです。
ベストプラクティス1:Read / Write の分離
先ほどのアンチパターンを、次のように書き換えます。
// 【正しい例】最初にまとめて読み取り、後からまとめて書き込む
const boxes = document.querySelectorAll(‘.box’);
// 1. まず「読み取り」をすべて終わらせる(ブラウザは値をキャッシュまたは遅延なしで返せる)
const widths = Array.from(boxes).map(box => box.offsetWidth);
// 2. その後で「書き込み」をまとめて行う(リフローは最後に1回だけ発生する)
boxes.forEach((box, index) => {
box.style.width = widths[index] + 10 + ‘px’;
});
これだけで、ブラウザの無駄な再計算を劇的に減らすことができます。
ベストプラクティス2:レイアウトを引き起こさないプロパティ(CSS Triggers)を使う
すべてのCSSプロパティがリフローを引き起こすわけではありません。
例えば、要素の位置を動かすときに `top` や `left` を変更すると、親要素からレイアウトが再計算されるため高負荷になります。
しかし、`transform` (translate) や `opacity` を使った場合、これらはレイアウトやペイントの工程をスキップし、GPUによる「Composite(合成)」のレイヤー処理だけで完結させることができます!
/ 【推奨】リフローを引き起こすプロパティは避ける /
.bad-animation {
position: absolute;
left: 0; / これを変更するとリフローが発生する! /
transition: left 0.3s ease;
}
/ 【推奨】transform を使えばリフローが発生しない(GPU加速) /
.good-animation {
transform: translateX(0); / リフローが起きない! /
transition: transform 0.3s ease;
}
アニメーションを実装する際は、極力 `transform` と `opacity` しか使わない、という気概を持つことがプロのフロントエンドエンジニアとしての第一歩です。
—
4. 実戦で役立つ!すぐに使えるモダンな回避テクニック
「とはいえ、どうしても要素のサイズを動的に取得してアニメーションさせたいんだよ!」というシーン、ありますよね。そんなときに使える実用的なヘルパー関数をシェアします。
`requestAnimationFrame` を活用し、ブラウザの描画タイミングに処理を同期させる手法です。
/
- 安全に要素の高さを取得してアニメーションさせる実用スニペット
- @param {HTMLElement} element 対象の要素
- @param {number} targetHeight 目標の高さ
/
function smoothResize(element, targetHeight) {
// 1. 現在の高さを「読み取り」(ここですぐ計算させない工夫)
const currentHeight = element.offsetHeight;
// 2. スタイルの適用(書き込み)を rAF でブラウザの描画サイクルに同期させる
requestAnimationFrame(() => {
element.style.height = `${targetHeight}px`;
});
}
また、モダンブラウザには `ResizeObserver` という強力なAPIもあります。DOM要素のサイズ変更を非同期で安全に監視できるため、従来の `window.resize` イベントでゲリラ的にDOMを触るような悪癖を完全に駆逐できます。
// ResizeObserver を使ったスマートな要素監視
const observer = new ResizeObserver(entries => {
for (let entry of entries) {
// ここでサイズ変更に応じた処理を安全に行う(ブラウザが最適化してくれる)
console.log(‘要素の新しいサイズ:’, entry.contentRect.width, entry.contentRect.height);
}
});
// 監視の開始
observer.observe(document.querySelector(‘.js-resizable-card’));
—
まとめ:パフォーマンスチューニングは「ブラウザへの思いやり」
フロントエンドのパフォーマンスチューニング本質は、難しいアルゴリズムを組むことではありません。「ブラウザがどういう仕組みでレンダリングしているか」を理解し、無駄な二度手間をかけさせないこと(=ブラウザへの思いやり)に他なりません。
- 「書き込み」と「読み取り」をコード内で交互に書かない(レイアウトスラッシングの防止)
- アニメーションや移動には `transform` と `opacity` を使い、リフローを避ける
- 重い処理やサイズ計測は `requestAnimationFrame` や `ResizeObserver` で適切にハンドリングする
この基本をチーム全体で意識するだけで、Webアプリケーションの体感速度は見違えるほど滑らかになります。
次のスプリントで「なんか重いな」と感じる画面に出会ったら、ぜひDevToolsを開いて、今回の知識を思い出してみてください。
それでは、快適なフロントエンドライフを!

コメント