実 DOM を変更しても、JavaScript が代入した瞬間に毎回ピクセルが描き直されるわけではありません。ブラウザは変更をまとめ、次のフレームに間に合うようにスタイル計算、レイアウト、描画、合成を進めます。この流れを知ると、「仮想 DOM は速い/実 DOM は遅い」という単純化を避け、実際のボトルネックを測って改善できます。
DOM から画面までの主な段階
ブラウザ内部は実装ごとに異なりますが、概念的には次の順序で画面を作ります。
HTML → DOM ─┐
├→ style 計算 → layout → paint → composite → 画面
CSS → CSSOM ─┘
- style 計算: 各要素にどの CSS 規則が適用されるかを求める
- layout(reflow): 要素の位置と大きさを決める
- paint: 背景、文字、枠線、影などの描画命令を作る
- composite: 複数のレイヤーを合成して画面へ出す
class の追加は style 計算を必要にする可能性があります。幅、高さ、文書内の構造を変えれば layout が必要になり得ます。色だけの変更でも paint は必要になるかもしれません。どの段階まで影響するかは CSS、要素の周辺、ブラウザの最適化に依存するため、プロパティ名だけから絶対的に判断しないことが大切です。
「実 DOM は遅い」は正確ではない
DOM API 自体が常に高コストなのではありません。ボタン一つのテキスト更新のような操作は、通常は問題になりません。問題になりやすいのは、巨大で複雑な DOM、広い範囲へ波及するレイアウト変更、同じフレーム内での不要な読み書きの往復です。
フレームワークの差分更新も、最後には実 DOM を更新します。仮想 DOM は「必要な変更を求めるための JavaScript 上の中間表現」であり、ブラウザのレイアウトや描画のコストを消すものではありません。計算量の小さい手続き的な DOM 操作が、汎用的な差分計算より適する場合もあります。
レイアウトスラッシングが起こる仕組み
ブラウザは DOM/CSS の書き込みを遅延できる一方、offsetWidth、getBoundingClientRect()、scrollHeight のような正確なレイアウト情報を JavaScript が読む必要があると、保留中の計算を先に完了させることがあります。
次のように、書き込みと読み取りをループ内で混ぜると、繰り返しレイアウトを強制する可能性があります。
for (const card of cards) {
card.style.width = `${container.offsetWidth / 3}px`;
}
必要な値を先に読み、書き込みをまとめます。
const width = container.offsetWidth / 3;
for (const card of cards) {
card.style.width = `${width}px`;
}
さらに設計できるなら、JavaScript で幅を配るのではなく CSS Grid や Flexbox に任せます。DOM 操作を少し最適化するより、レイアウトを宣言的な CSS に移す方が堅く、画面サイズの変更にも強いことが多いです。
アニメーションではフレームを意識する
連続的に変わる UI では requestAnimationFrame を使うと、ブラウザの描画タイミングに合わせて更新できます。
function move(timestamp) {
const progress = Math.min(timestamp / 300, 1);
element.style.transform = `translateX(${progress * 160}px)`;
if (progress < 1) requestAnimationFrame(move);
}
requestAnimationFrame(move);
ただし通常の UI アニメーションは、可能なら CSS transition / animation を優先します。transform と opacity はレイアウトを変更せず合成だけで済む場合があるため、位置やサイズを毎フレーム変更するより滑らかになりやすい、という性質があります。実際の結果は DevTools の Performance パネルで確認します。
測定してから直す
体感の悪さには、JavaScript の長い処理、画像のデコード、ネットワーク待機、レイアウト、描画など複数の原因があります。DOM ノード数だけ、または仮想 DOM の有無だけを犯人にしないでください。
調査では次の順で見ると整理しやすくなります。
- 操作から表示までの記録を Performance パネルで取る
- 長い JavaScript タスク、Layout、Paint のどれが時間を使うかを特定する
- 同じ操作で何度もレンダー・計測・DOM 書き換えが起きていないか確認する
- 変更後も同じシナリオで再測定する
最適化は、読み書きの分離、表示件数の仮想化、コンポーネント境界の見直し、画像や CSS の改善など、観測した原因に対応させます。
まとめ
実 DOM の変更は、style、layout、paint、composite というブラウザの仕事につながります。高コストなのは DOM 操作というラベルではなく、変更の影響範囲と、計測を挟んだ不必要な再計算です。フレームワークの更新方式を評価するときも、最終的なブラウザの仕事を測定の対象にしましょう。