Kodokon kodokon.com

レンダリングパイプライン: リフロー、リペイント、コンポジット

ブラウザのレンダリングの4つのフェーズを理解し、どのCSSプロパティが高コストなのか、そして will-change が内部でどのように働くのかを正確に把握しましょう。

11 分 · 3 問

このレッスンを Kodokon で開く

ブラウザはCSSを、4つのフェーズからなるパイプラインを通じてピクセルへと変換します。スタイル(値の計算)、レイアウト(ジオメトリ、リフローとも呼ばれる)、ペイント(ピクセルへのラスタライズ、すなわちリペイント)、そしてコンポジット(GPU上でのレイヤーの合成)です。黄金律はこうです。触れるフェーズが早いほど請求は大きくなる。なぜなら、それ以降のフェーズがすべてやり直しになるからです。widthtop の変更はレイアウト + ペイント + コンポジットを引き起こします。colorbox-shadow の変更はレイアウトを飛ばしますが、リペイントは発生します。コンポジタスレッドだけで処理できるのは transformopacity だけです。JavaScriptによってメインスレッドがブロックされているときでさえ、アニメーションは滑らかなままです。

CSS
.slide-bad {
  position: absolute;
  transition: left 0.3s ease;
}
.slide-bad:hover { left: 120px; }

.slide-good {
  transition: transform 0.3s ease;
}
.slide-good:hover { transform: translateX(120px); }
見た目が同じ2つのアニメーション。1つ目は毎フレームでリフローを強制し、2つ目はコンポジタ上にとどまります。

ブラウザは怠惰です。スタイルの書き込みをまとめておき、次のリフレッシュのときにだけレイアウトを再計算します。しかし一部のJavaScriptの読み取り、たとえば offsetHeightgetBoundingClientRect()scrollTop、ジオメトリに関わるプロパティに対する getComputedStyle() は、最新の値を要求します。書き込みが保留中の場合、ブラウザは即座に同期的に再計算しなければなりません。これが強制リフローです。ループの中で読み取りと書き込みを交互に行うと、レイアウトスラッシングが発生します。バッチ全体で1回のリフローで済むはずが、反復ごとに完全なリフローが1回ずつ走ってしまうのです。

JAVASCRIPT
const items = document.querySelectorAll('.item');

items.forEach((item) => {
  const h = item.offsetHeight;
  item.style.height = h * 2 + 'px';
});

const heights = [...items].map((i) => i.offsetHeight);
items.forEach((item, index) => {
  item.style.height = heights[index] * 2 + 'px';
});
バージョン1: 要素ごとに強制リフローが1回。バージョン2: まずすべて読み取り、それからすべて書き込む。リフローは1回だけ。

コンポジットはページをレイヤーに切り分けます。各レイヤーは独立してラスタライズされ、GPUによって合成されます。will-change: transform は、アニメーションの前に要素を独自のレイヤーへ昇格させるよう要求し、最初のフレームでの高コストなラスタライズを回避します。見落とされがちな仕様上の細部として、will-change は、それが予告するプロパティと同じ副作用を生じさせなければなりません。したがって will-change: transformスタッキングコンテキストを作り出し、その position: fixed の子孫に対して要素を包含ブロックへと変えます。まさに本物の transform と同じです。単純なパフォーマンスヒントが、スタッキング順序や固定要素を壊してしまうことがあるのです。

JAVASCRIPT
const panel = document.querySelector('.panel');

panel.addEventListener('pointerenter', () => {
  panel.style.willChange = 'transform';
});

panel.addEventListener('transitionend', () => {
  panel.style.willChange = 'auto';
});
仕様が推奨するパターン。アニメーションの直前に昇格させ、直後に解放します。

理解度チェック

このレッスンの要点をしっかり覚えているか確認しましょう。

  1. リフローもリペイントも伴わず、コンポジタスレッドだけで完全に処理できるアニメーションはどれですか?
    • height を 0 から 200px へ
    • margin-left を 0 から 100px へ
    • transform: translateX(100px)
    • ホバー時の box-shadow
  2. スタイルも変更するループの中で offsetHeight を読み取ると、なぜそれほど高コストなのですか?
    • 書き込みが保留中の場合、読み取りごとに同期的なリフローが強制される
    • offsetHeight はアクセスのたびにGPUリペイントを引き起こす
    • このプロパティはJavaScriptエンジンによって決してキャッシュされない
  3. will-change: transform が本物の transform と共有する副作用はどれですか?
    • 要素のCSSトランジションを無効化する
    • スタッキングコンテキストと、position: fixed のための包含ブロックを作り出す
    • 要素を display: block に強制する
    • ラスタライズの解像度を2倍にする