ブラウザのレンダリングパイプラインとパフォーマンス最適化について
現代のウェブアプリケーションでは、気づけばレイアウトスラッシングやパフォーマンス低下を引き起こすコードを書いていて動作が重くなることがあります。
DOMのちょっとした変更がUIのカクつきやレスポンス低下を引き起こすこともあります。
その根本原因を理解するためには、レンダリングパイプラインを正しく把握することが重要です。
本記事ではレンダリングの仕組みと、それを踏まえた実践的なパフォーマンス最適化手法をまとめていきます。
レンダリングパイプラインとは
ブラウザはHTMLとCSSを元に、以下の流れで画面をレンダリングします。
Reflow / Layout>Repaint / Paint>Compositeで上流の処理に戻れば戻るほどコストが高く、パフォーマンス低下を引き起こします。そのため、不要な巻き戻りを避けることがパフォーマンス改善につながっていきます。
- DOM の生成
- CSSOM の生成
- レンダーツリー構築
- レイアウト(Reflow / Layout):各要素の位置・サイズを計算します。
- ペイント(Repaint / Paint):背景やテキストをピクセルへ描画します。
- コンポジット(Composite):ペイントされたレイヤーを GPU 等で合成します。
処理の上流に戻るほどコストが高いため、不要な巻き戻りを避けることがパフォーマンス改善につながります。
Reflow / Repaint / Composite の違い
できる限り Composite だけで済む変更 に抑えると良いです。
| 処理 | 内容 | コスト |
|---|---|---|
| Reflow(レイアウト) | 幾何計算を再実行 | 高 |
| Repaint(再描画) | 見た目のみ再描画 | 中 |
| Composite(合成) | レイヤーを再合成 | 低 |
どの変更がどこに影響するか
| 変更内容 | Reflow | Repaint | Composite |
|---|---|---|---|
| 幅・高さの変更 | ● | ● | ● |
| 背景色の変更 | ● | ● | |
| transform / opacity | ● | ||
| DOM の追加・削除 | ● | ● | ● |
パフォーマンスを落とさないための最適化
1. レイアウトスラッシングの回避
読み取りと書き込みを分離してレイアウトスラッシングの回避します。
悪い例:
const items = document.querySelectorAll('.item');
for (const el of items) {
const w = el.offsetWidth; // 読み取り
el.style.width = (w + 10) + 'px'; // 書き込み
}
改善例:
const items = document.querySelectorAll('.item');
const widths = items.map(el => el.offsetWidth); // 読み取りのみ
widths.forEach((w, i) => {
items[i].style.width = (w + 10) + 'px'; // 書き込みのみ
});
2. アニメーションは transform / opacity を使う
top / left を変更するとレイアウト処理が走るので、transform, opacityを使用します。
.move {
transition: transform 300ms;
}
el.classList.add('move');
el.style.transform = 'translateY(150px)';
3. スタイルはクラス切り替えでまとめる
それぞれで変更を加えるより、クラスで一括してスタイルを変更したほうがパフォーマンス的に良い
.dark {
background: #222;
color: white;
border-color: #444;
}
el.classList.add('dark');
4. DOM 操作はまとめる
DocumentFragment や innerHTML を活用します。
const frag = document.createDocumentFragment();
for (let i = 0; i < 500; i++) {
const li = document.createElement('li');
li.textContent = `Row ${i}`;
frag.appendChild(li);
}
list.appendChild(frag);
5. requestAnimationFrame() で更新タイミングを制御
ブラウザのフレームと同期します。
requestAnimationFrame(() => {
box.style.transform = 'translateX(100px)';
});
6. 必要な時だけwill-changeを使用する
will-changeを永続的に使用するとメモリ浪費を招きます。
el.style.willChange = 'transform';
requestAnimationFrame(() => {
el.style.transform = 'scale(1.2)';
});
el.addEventListener('transitionend', () => {
el.style.willChange = '';
});
7. content-visibility / contain で影響範囲を限定
表示領域外のレンダリングを省略できます。
.card {
content-visibility: auto;
contain-intrinsic-size: 600px;
}
8. display:none と visibility:hidden を使い分ける
displayを使用すると構造が破壊されるので、レイアウト破壊を避けたい時はvisibilityを使用します。
el.style.visibility = 'hidden'; // Repaint のみ
// layout にも影響させたい時だけ
el.style.display = 'none'; // Reflow 発生
9. resize / scroll はデバウンス・スロットル
頻発するイベントでReflowが多発するのを防ぎます。
let ticking = false;
window.addEventListener('scroll', () => {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => {
const rect = el.getBoundingClientRect();
indicator.style.width = rect.width + 'px';
ticking = false;
});
});
10. 長いリストは仮想化する
// translateY で合成に寄せる
pool.style.transform = `translateY(${offsetY}px)`;
pool.innerHTML = html; // 一括更新
11. 強制レイアウト処理を避ける
下記APIを使用すると強制レイアウト処理が走るので注意が必要です。
offsetWidthoffsetHeightgetBoundingClientRect()-
scrollTop/scrollHeightetc.
DevTools による検証方法
Chrome DevTools → Performanceで確認します。
また、Rendering タブ → Paint Flashing を有効にすると再描画領域が可視化できます。
- Recalculate Style
- Layout
- Paint
- Composite Layers
まとめ
ブラウザの仕組みを理解した実装は、ユーザー体験を大きく改善します。
継続的に最適化することを心がけていきましょう。
参考
Discussion