リフローとリペイントを引き起こすCSSプロパティの分類
どのCSS変更が重いかを段ごとに即断でき、アニメーションのカクつきを根本から潰せるようになる。プロパティをレイアウト/ペイント/コンポジットに分類し、レイアウトスラッシングの原理と回避策まで解説します。
- 1.CSSプロパティ変更はレイアウト(ジオメトリ再計算)→ペイント(描画命令再記録)→コンポジット(GPU再合成)のどこから無効化を起こすかで重さが決まり、起点が上流なほど下流の全段が連鎖して走る。
- 2.width / top / margin など幾何に効く値はレイアウト起点で最重量、color / background / box-shadow はペイント起点、transform / opacity はコンポジットのみで完結し最軽量。
- 3.レイアウトスラッシングは「書き込み直後にジオメトリを読む」を繰り返すと強制同期レイアウトが多発する現象で、読みと書きをフェーズ分離し rAF に揃えれば消える。
3段分類という考え方
CSSプロパティを変更したとき、ブラウザがどれだけ仕事をするかは「どの段から再計算が始まるか」で決まります。レンダリングパイプラインは大きくレイアウト(Layout / リフロー)→ペイント(Paint / リペイント)→コンポジット(Composite / 合成)の順に流れ、ある段の入力が変わるとその段以降がすべてやり直しになります。したがってプロパティは、変更がどの段を起点に無効化を起こすかで3群に分類できます。段順と無効化の伝播そのものは レンダリングパイプライン詳説 と ブラウザのレンダリング で詳しく扱っていますが、本稿は個々のプロパティの仕分けと、レイアウトスラッシングの発生原理に焦点を絞ります。
| 起点の段 | 意味 | 走る工程 | 相対コスト |
|---|---|---|---|
| レイアウト | ジオメトリ(位置・寸法)が変わる | レイアウト→ペイント→合成(全段) | 最重量 |
| ペイント | 見た目だけ変わり寸法は不変 | ペイント→合成 | 中 |
| コンポジット | 既存テクスチャの変換だけ変わる | 合成のみ | 最軽量 |
鍵は「起点が上流なほど、下流の段がただ乗りで全部走る」点です。レイアウトを起こす変更は必ずペイントとラスタライズと合成も誘発します。逆にコンポジット起点なら、上流の重い2段を丸ごとスキップできます。
レイアウトを起こすプロパティ(最重量)
要素のボックスの幾何を左右する値は、変更するとレイアウトツリーの再計算(リフロー)から始まります。レイアウトは双方向の依存を持つため重く、1要素の寸法変化が祖先・兄弟・子孫へ波及します。
代表例は次の通りです。これらは「サイズ・位置・フローに影響するか」で見分けられます。
寸法系: width, height, min/max-width, min/max-height, padding, border-width
位置系: top, left, right, bottom, margin
表示系: display, float, position, overflow
テキスト系: font-size, font-family, line-height, text-align, white-space, vertical-align
内在系: フレックス/グリッドの各種(flex-basis, grid-template など)
書き込みだけでなく、特定プロパティの読み取りもレイアウトを強制します。offsetTop / offsetWidth / clientHeight / scrollTop / getBoundingClientRect() / getComputedStyle() の幾何系プロパティは、最新のジオメトリを返すために保留中のレイアウトをその場で完了させます。読み取りが副作用を持つ、というのが直感に反するポイントです。
ペイントを起こすプロパティ(中量)
寸法を変えずに見た目だけを変える値は、レイアウトをスキップしてペイント段から再計算します。ペイント段は実際に塗るのではなく、各レイヤーの描画命令のリスト(display list)を再記録する工程です(詳細は レンダリングパイプライン詳説 参照)。レイアウトを飛ばせる分レイアウト起点より軽いですが、対象領域のラスタライズをやり直すため無料ではありません。
color, background-color, background-image, background-position
border-color, border-style, box-shadow, outline
visibility(hidden は領域を保持するのでレイアウト不変)
border-radius, text-decoration(色やスタイルの変更)
ペイント起点でも、ぼかしを伴う指定(大きな box-shadow のブラー、filter: blur()、半透明グラデーション)はラスタライズが重く、面積が広いと中量どころか体感で重くなります。ペイント面積(ピクセル数)とぼかし半径が効くため、「ペイント=軽い」と一括りにせず、DevTools の Paint flashing で再ペイントされた範囲を実測してください。
コンポジットのみで完結するプロパティ(最軽量)
transform と opacity は、要素が合成レイヤーへ昇格していれば、レイアウトもペイントも起こさずコンポジタスレッドの再合成だけで反映できます。理由は、これらの値がペイント段の display list ではなくプロパティツリーのノードとして保持され、値の差し替えだけで合成へ渡せるからです。
/* 良い:合成のみで完結。リフローもリペイントも発生しない */
.box { will-change: transform; }
.box:hover { transform: translateX(20px) scale(1.05); opacity: 0.8; }
/* 悪い:見た目は近いが left/width はレイアウト起点で全段が走る */
.box-bad:hover { left: 20px; width: 105%; }
transform / opacity のアニメーションはコンポジタスレッドで駆動できるため、メインスレッドが重いJSで詰まっていても 60fps を保てます。位置を left で動かすと毎フレーム メインスレッドのレイアウトが要り、JSが詰まれば即カクつきます。「動かすなら transform、消すなら opacity」が原則です。
レイアウトスラッシングの発生原理
レイアウトスラッシング(layout thrashing、強制同期レイアウト)は、1フレーム内でレイアウトが何度も走って急激にカクつく現象です。原理はブラウザのレイアウトのバッチ化にあります。
通常ブラウザは、スタイル書き込みを溜め込み、フレーム末でまとめて1回レイアウトします。ところが書き込みの直後にジオメトリを読むと、ブラウザは「正しい値を返すために今すぐレイアウトせねば」と判断し、その場で同期実行します。これをループ内で繰り返すと、要素数ぶんレイアウトが走ります。
// 悪い:読み(offsetHeight)と書き(height)が交互 → 反復ごとに強制同期レイアウト
for (const el of boxes) {
const h = el.offsetHeight; // 保留中のレイアウトを完了させる(読み)
el.style.height = h * 2 + 'px'; // レイアウトを汚す(書き)→ 次の読みでまた完了
}
このコードは要素が N 個なら N 回レイアウトが走り、計算量が要素数に比例して悪化します。原因は読みと書きがインターリーブしている点だけで、処理内容自体は変わりません。
強制同期レイアウトが起きるのは「保留中のスタイル変更がある状態で、ジオメトリ系プロパティを読んだとき」です。書き込みが一切なければ、offsetTop を何度読んでもレイアウトは1回で済みます(結果がキャッシュされる)。問題は読み単独ではなく、書き→読み→書き→読みの往復構造にあります。
回避策:読み書きフェーズ分離
解決は単純で、すべての読みを先に済ませ、すべての書きを後でまとめることです。間に書きが挟まらなければ、読みはキャッシュ済みの単一レイアウトから返り、書きはフレーム末の1回に集約されます。
// 良い:読みフェーズ → 書きフェーズに分離 → レイアウトはフレームで1回
const heights = boxes.map(el => el.offsetHeight); // 読みをまとめる
boxes.forEach((el, i) => { // 書きをまとめる
el.style.height = heights[i] * 2 + 'px';
});
実務では次の3点を併用します。
| 手法 | 効果 | 使いどころ |
|---|---|---|
| read→write 分離 | 強制同期レイアウトを根絶 | ループでDOMを測って更新する全処理 |
| requestAnimationFrame | 書き込みをフレーム境界へ集約 | アニメーションやスクロール連動の更新 |
| contain / content-visibility | 無効化の波及範囲を物理的に切る | 独立して更新される領域の囲い込み |
contain: layout は内部レイアウトを外と分離し、子の寸法変化が祖先へ波及するのを止めます。content-visibility: auto は画面外要素のレイアウトとペイントを遅延します。いずれも無効化の伝播範囲を縮める方向の道具で、スラッシング対策の読み書き分離と相補的です。FLIP テクニック(最終状態を一度測ってから差分を transform で再生する手法)も、測定を1回に閉じ込めてアニメーション本体を合成のみに寄せる、同じ思想の応用です。
実務での仕分け手順
パフォーマンス問では、(1) プロパティの3段分類(幾何=レイアウト、見た目=ペイント、transform/opacity=合成)、(2) 起点が上流なほど下流が連鎖する伝播原則、(3) 強制同期レイアウトの発生条件(書いた直後にジオメトリを読む)、(4) read→write 分離と rAF による回避、の4点が頻出です。「visibility:hidden はレイアウト不変、display:none はリフロー発生」の対比もよく問われます。
- 動かす前に分類する:変更したいプロパティが幾何に触れるなら最重量。同じ見た目を
transform/opacityで代替できないか先に検討する。 - 読み書きを分離する:DOM測定と更新をフレーム内で混ぜない。混ぜると要素数に比例してレイアウトが増える。
- 範囲を囲う:頻繁に更新する領域へ
containを当て、リフロー・リペイントの波及を物理的に断つ。 - 実測する:DevTools の Performance パネルで Layout / Paint のイベントを、Paint flashing で再描画範囲を確認する。
まとめ
CSSプロパティは、変更がどの段から無効化を起こすかで3群に分かれます。width / top / margin など幾何系はレイアウト起点で全段が連鎖し最重量、color / box-shadow など見た目系はペイント起点で中量、transform / opacity はコンポジットのみで完結し最軽量です。レイアウトスラッシングは「書き込み直後にジオメトリを読む」往復が強制同期レイアウトを多発させる現象で、読みと書きのフェーズ分離と requestAnimationFrame、contain による範囲の囲い込みで解消できます。さらに踏み込むなら CSS のカスケードと Web パフォーマンス の計測手法を合わせて、分類から実測・改善までを一本の筋道で押さえてください。
Web/フロントエンド Article
リフローとリペイントを引き起こすCSSプロパティの分類を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
CSS
比較で見る軸
難易度: advanced / カテゴリ: Web/フロントエンド / タグ数: 5
導入後に効く点
width / top / margin など幾何に効く値はレイアウト起点で最重量、color / background / box-shadow はペイント起点、transform / opacity はコンポジットのみで完結し最軽量。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- Web/フロントエンド
- タグ数
- 5
判断チェックリスト
- 自社の用途が「CSS / パフォーマンス」に近いか確認する。
- 強みである「CSSプロパティ変更はレイアウト(ジオメトリ再計算)→ペイント(描画命令再記録)→コンポジット(GPU再合成)のどこから無効化を起こすかで重さが決まり、起点が上流なほど下流の全段が連鎖して走る。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。