こんにちは、Ryohei(@ityryohei)です!

Webサイトの体感速度を最も左右する要因の一つが、JavaScriptによるDOM(Document Object Model)操作の仕方です。DOM操作は、HTML要素の追加、削除、属性変更などを行う基本的な処理ですが、非効率に行うとブラウザに大きな負荷がかかり、画面がカクついたり(フリーズ)、動作が重くなったりします。

これは、DOM操作のたびにブラウザが実行するレンダリング(再描画)の仕組みに原因があります。

本記事では、「なぜDOM操作は遅くなるのか」というブラウザの仕組み(リフロー/リペイント)を解説し、Webサイトを高速化するための「操作回数を最小限にする」実用的なテクニックと、避けるべき「パフォーマンスの禁止事項」を徹底解説します。

JavaScriptで要素を追加すると画面がカクつくのはなぜ?DOM操作を速くするにはどうすればいい?

上記の疑問にお答えします。

では、解説していきます。

基礎知識:DOM操作がWebサイトを遅くする理由

DOM操作がパフォーマンスに影響を与える理由は、ブラウザのレンダリングプロセスが繰り返し実行されることにあります。

レンダリングのコスト:リフローとリペイント

ブラウザは、HTMLやCSSの変更に応じて画面を更新する際、以下のコストの高い2つの処理を実行します。

処理定義発生条件
リフロー (Reflow)要素の配置やサイズが変更され、他の要素の位置やサイズに影響を与える際の再計算。要素の追加・削除、サイズ変更、ウィンドウのリサイズなど。 最もコストが高い。
リペイント (Repaint)要素の色や背景など、レイアウトに影響しない視覚的変更があった際の再描画。color, background-color, visibilityなどの変更。

JavaScriptでDOM要素を一つずつ追加したり、スタイルを頻繁に書き換えたりする行為は、ブラウザに何度もリフローやリペイントを実行させ、処理の遅延を招きます。

ブラウザの最適化(キュー)

ブラウザは、パフォーマンスを維持するために、連続するDOM操作をすぐに実行せず、キュー(待ち行列)に貯めておき、まとめて一回のリフローで処理しようとします。しかし、以下の操作を行うと、ブラウザはキューを無視し、強制的にリフローを実行します。

  • element.offsetWidthelement.clientHeight などのレイアウト情報を取得する操作。
  • window.getComputedStyle() など、最新の計算済みスタイルを取得する操作。

これは、「最新の正確な情報が必要」とブラウザが判断するためで、ループ内でこれらの情報取得を行うと、パフォーマンスは致命的に低下します。

実践テクニック:DOM操作を「まとめて一回」にする戦略

パフォーマンスを最大化する原則は、「ブラウザにリフローを命じる回数を最小限にする」ことです。

DocumentFragmentの活用(仮想DOMの原型)

DocumentFragment(ドキュメントフラグメント)は、メモリ上でのみ存在する一時的なDOMノードのコンテナです。要素をまとめて作成し、最後にDOMツリーに挿入することで、リフローをたった一回に抑えられます。

  1. フラグメントを作成し、その内部で全てのDOM操作を行う。
  2. 最後に、作成したフラグメント全体をDOMツリーに挿入する。

JavaScript

const listContainer = document.getElementById('my-list');
const fragment = document.createDocumentFragment(); // メモリ上にコンテナを作成

for (let i = 0; i < 100; i++) {
  const item = document.createElement('li');
  item.textContent = `項目 ${i}`;
  fragment.appendChild(item); // 内部で操作。リフローは発生しない
}

listContainer.appendChild(fragment); // 最後に一回だけDOMに追加(リフローは一回)

HTML文字列によるバッチ挿入

大量の要素を追加する場合、HTMLの文字列として構成し、最後にinnerHTMLプロパティでまとめて挿入する手法も非常に高速です。

JavaScript

const listContainer = document.getElementById('my-list');
let htmlString = '';

for (let i = 0; i < 100; i++) {
  htmlString += `<li>項目 ${i}</li>`; // HTML文字列を作成
}

listContainer.innerHTML = htmlString; // DOM操作を一回にまとめる(高速)
  • 注意点:innerHTMLはセキュリティリスク(XSS攻撃)があるため、信頼できるソースからの文字列のみに使用してください。

CSSクラスによるスタイルの一括変更

JavaScriptで複数のCSSプロパティを一つずつ変更するのは避けましょう。それぞれの変更がリペイントを発生させる可能性があります。

代わりに、あらかじめCSSで定義したクラスを一つだけ切り替えることで、変更を最小限に抑えます。

JavaScript

// スタイルを一括で変更
element.classList.add('is-active'); 

CSS

/* CSS側でスタイルをまとめて定義しておく */
.is-active {
  opacity: 1;
  background-color: blue;
  transform: translateX(10px);
}

パフォーマンスを低下させる「禁止事項」と回避策

無意識のうちにリフローを強制実行させ、パフォーマンスを低下させるコーディング習慣を避けましょう。

【禁止事項 1】ループ内でのスタイル変更と情報取得

最もパフォーマンスを破壊するパターンです。要素のサイズ変更(style.width = '100px')と、その後のサイズ取得(offsetWidth)を交互にループ内で行うと、ループごとに強制的なリフローが発生します。

JavaScript

const boxes = document.querySelectorAll('.box');

boxes.forEach(box => {
  // スタイル変更(リフロー待機)
  box.style.width = '100px'; 
  
  // レイアウト情報取得(キューをクリアし、強制リフロー発動)
  console.log(box.offsetWidth); 
}); // 100要素あれば100回リフローが発生する

【禁止事項 2】レイアウトに影響するアニメーション

width, height, left, topといったレイアウトを変更するプロパティをJavaScriptで頻繁に更新してアニメーションを実現すると、毎フレームリフローが発生し、動作が重くなります。

  • 回避策:アニメーションには、リフローを起こさないプロパティ(transformopacityなど)を使用しましょう。これらはGPU(グラフィックボード)で処理されることが多く、非常に高速です。

【禁止事項 3】親ノードがいない状態での要素操作

DOMツリーに追加されていない要素(親ノードがない状態)に対して大量の操作を行った後、最後にDOMに挿入すること自体は問題ありませんが、CSSの計算が必要な操作を行うと、無駄な処理が発生することがあります。

要素の操作は、DocumentFragment内など、親要素の内部で完結させることを徹底しましょう。

最後に

JavaScriptによるDOM操作のパフォーマンスを向上させる鍵は、ブラウザの仕組み(リフローとリペイント)を理解し、「いかにまとめて操作するか」に尽きます。

  • 原則:DOM操作は、DocumentFragmentinnerHTMLを使って、一括で行いましょう。
  • 厳禁:ループ内で要素のレイアウト情報を取得したり、スタイルを個別に変更したりするのはやめましょう。
  • 最適化:アニメーションにはtransformopacityを使い、GPUの力を借りましょう。

これらのシンプルなテクニックを実践するだけで、あなたのWebサイトの体感速度とユーザー体験は大きく向上するはずです。

以上、DOM操作の効率化とWebサイト高速化テクニックのご紹介でした!

この記事を書いた人

Ryohei

Webエンジニア / ブロガー

福岡のWeb制作会社に務めるWebエンジニアです。エンジニア歴は10年程で、好きな言語はPHPとJavaScriptです。本サイトは私がインプットしたWebに関する知識を整理し、共有することを目的に2015年から運営しています。Webに関するご相談があれば気軽にお問い合わせください。