3秒でわかる
HTML や CSS の情報をブラウザが画面上のピクセルに変換する一連の処理。表示の速さや引っかかりはこの流れのどこかで決まります。
もう少し詳しく
どういうものか
レンダリングは、ブラウザが受け取った HTML と CSS を解釈して、実際の画面に絵を描くまでの処理です。大まかにはパース、スタイル計算、レイアウト、ペイント、合成の順に進みます。パースで HTML から DOM ツリー、CSS から CSSOM を作り、スタイル計算で各要素に適用される値を決め、レイアウトで座標と大きさを算出し、ペイントで色や文字を塗り、合成で複数のレイヤを重ねて 1 枚の画面にします。
なぜ必要か
この流れを知っていると、遅さの原因を当てられます。表示が始まるまでが遅いなら、パースを止めている同期スクリプトや CSS の読み込みを疑います。スクロールが引っかかるなら、毎フレームでレイアウトが走っている可能性を疑います。どの段階で時間を使っているかによって、打つ手がまったく違います。
コストの大きさにも順序があります。レイアウトが最も重く、ペイント、合成の順に軽くなります。位置を left で動かすとレイアウトからやり直しになりますが、transform で動かすと合成だけで済むため、アニメーションの滑らかさが変わります。
具体例
HTML ──parse──▶ DOM ─┐
├─▶ スタイル計算 ─▶ レイアウト ─▶ ペイント ─▶ 合成
CSS ──parse──▶ CSSOM ┘// 悪い例。読み取りと書き込みが交互で、毎回レイアウトが強制される
for (const el of items) {
el.style.width = el.offsetWidth + 10 + "px";
}
// 読み取りをまとめてから書き込む
const widths = items.map((el) => el.offsetWidth);
items.forEach((el, i) => { el.style.width = widths[i] + 10 + "px"; });つまずきやすいところ
offsetWidth や getBoundingClientRect の読み取りは、直前にスタイルを変更していると、その場でレイアウトを実行させます。ループの中で読み書きを交互に行うと、これが要素の数だけ繰り返されて一気に重くなります。強制同期レイアウトと呼ばれる現象です。
もうひとつは画像です。幅と高さを指定しない画像は、読み込み完了時に高さが確定して周囲を押し下げます。これが Core Web Vitals の CLS として計測されます。width と height、または aspect-ratio を先に指定しておけば起きません。
覚え方
レイアウトは位置を決める工程、ペイントは色を塗る工程、合成は重ねる工程。動かすなら塗り直しの手前で済ませる、と覚えると transform を選ぶ理由が腑に落ちます。
