ブラウザのレンダリングの4つのフェーズを理解し、どのCSSプロパティが高コストなのか、そして will-change が内部でどのように働くのかを正確に把握しましょう。
このレッスンを Kodokon で開くブラウザはCSSを、4つのフェーズからなるパイプラインを通じてピクセルへと変換します。スタイル(値の計算)、レイアウト(ジオメトリ、リフローとも呼ばれる)、ペイント(ピクセルへのラスタライズ、すなわちリペイント)、そしてコンポジット(GPU上でのレイヤーの合成)です。黄金律はこうです。触れるフェーズが早いほど請求は大きくなる。なぜなら、それ以降のフェーズがすべてやり直しになるからです。width や top の変更はレイアウト + ペイント + コンポジットを引き起こします。color や box-shadow の変更はレイアウトを飛ばしますが、リペイントは発生します。コンポジタスレッドだけで処理できるのは transform と opacity だけです。JavaScriptによってメインスレッドがブロックされているときでさえ、アニメーションは滑らかなままです。
.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); }ブラウザは怠惰です。スタイルの書き込みをまとめておき、次のリフレッシュのときにだけレイアウトを再計算します。しかし一部のJavaScriptの読み取り、たとえば offsetHeight、getBoundingClientRect()、scrollTop、ジオメトリに関わるプロパティに対する getComputedStyle() は、最新の値を要求します。書き込みが保留中の場合、ブラウザは即座に同期的に再計算しなければなりません。これが強制リフローです。ループの中で読み取りと書き込みを交互に行うと、レイアウトスラッシングが発生します。バッチ全体で1回のリフローで済むはずが、反復ごとに完全なリフローが1回ずつ走ってしまうのです。
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';
});コンポジットはページをレイヤーに切り分けます。各レイヤーは独立してラスタライズされ、GPUによって合成されます。will-change: transform は、アニメーションの前に要素を独自のレイヤーへ昇格させるよう要求し、最初のフレームでの高コストなラスタライズを回避します。見落とされがちな仕様上の細部として、will-change は、それが予告するプロパティと同じ副作用を生じさせなければなりません。したがって will-change: transform はスタッキングコンテキストを作り出し、その position: fixed の子孫に対して要素を包含ブロックへと変えます。まさに本物の transform と同じです。単純なパフォーマンスヒントが、スタッキング順序や固定要素を壊してしまうことがあるのです。
const panel = document.querySelector('.panel');
panel.addEventListener('pointerenter', () => {
panel.style.willChange = 'transform';
});
panel.addEventListener('transitionend', () => {
panel.style.willChange = 'auto';
});