作成日:2025/7/30,最終更新日:2025/8/6

JavaScript / TypeScriptのDOM操作とタイミングの深い関係

DOM操作のタイミングに関する解説

この記事では、DOMContentLoaded, load, 非同期処理などのタイミングとは何かについて詳しく解説します。


JavaScriptでDOMを操作するうえで欠かせないのが「いつDOMにアクセスできるのか?」というタイミングの理解です。
DOMContentLoadedload の違い、scriptタグの位置や属性(defer / async)による影響、さらには setTimeoutrequestAnimationFrame を使った非同期的な操作まで、DOMとタイミングに関する知識をわかりやすく整理していきます。


1. DOMContentLoadedとloadの違い

解説: ページが読み込まれたタイミングを知るイベントとして、DOMContentLoadedload の2つがよく使われます。
両者の違いを理解することは、正しいタイミングでDOM操作を行うためにとても重要です。




● DOMContentLoaded:HTMLの解析完了を検知

このイベントは、HTML文書の解析が完了した時点で発火します。
画像やCSSなどのリソースが読み込まれていなくても、DOMが構築されたら即実行できるのがポイントです。

document.addEventListener('DOMContentLoaded', () => {
  console.log('DOMの構築が完了しました');
});

もっとも早い段階でDOMにアクセスできるので、初期化処理要素取得にはこのイベントが推奨されます。


● load:ページ内の全リソースが読み込み完了してから

一方、load イベントは、画像やCSS・iframeなどのすべてのリソースが読み込まれたあとに発火します。

window.addEventListener('load', () => {
  console.log('ページのすべてのリソースが読み込まれました');
});

大きな画像や外部スクリプトの読み込みが遅いと、loadイベントまでのタイムラグが発生する点には注意が必要です。


● 比較まとめ

イベント発火タイミング主な用途
DOMContentLoaded HTMLの構文解析が完了した時点 要素取得、初期化処理
load 画像やCSSなどの全リソースが読み込まれた後 ページ全体の表示完了後に実行したい処理

● 実行例:タイミングの違いを見てみよう

以下のコードを実行すると、それぞれのタイミングでメッセージが表示されます。
DOMContentLoaded はすぐに、load は全リソース読み込み後に表示されるはずです。
※consoleに出力されるので、ブラウザの開発者ツールを開いて確認してください。
※スマホでもChromeなら、chrome://inspectでconsoleを確認できます!
 chrome://inspectをアドレスバーに入力して、「ログ記録を開始」を押すと記録が始まって確認できるようになります。


● まとめ

  • DOMContentLoaded:DOM要素へのアクセスが可能になる最速のタイミング
  • load:ページ全体のリソース読み込み完了を待って実行される
  • ✅ 通常の初期処理は DOMContentLoaded が適している

2. scriptタグの書き方と読み込みタイミング

解説: JavaScriptの読み込み・実行タイミングは、<script> タグの「書き方」によって大きく変わります。
特にdefer属性type="module"の違いは、DOM操作の成否にも影響します。ここではその仕組みを解説します。




● 基本構文と使い方

代表的な2つの指定方法は、次のとおりです。

// defer を使う場合
<script src="main.js" defer></script>

// モジュールとして読み込む場合
<script type="module" src="main.js"></script>

● defer属性:HTML解析完了後に順序通り実行

defer を付けると、HTMLの読み込みと並行してスクリプトを取得し、DOM構築完了後に実行されます。
複数のスクリプトがある場合も、記述順に実行されるのが特徴です。

<script src="script1.js" defer></script>
<script src="script2.js" defer></script>

上記のようにすると、HTML構文解析後に script1.jsscript2.js の順で実行されます。


● type="module":モジュール構文 + defer相当の動作

type="module" を指定すると、ES Modulesとして読み込まれます。
こちらも HTML構文解析後に自動実行される点で defer と似ていますが、次の特徴があります:

  • ✅ 自動的に strict mode が有効
  • ✅ 外部ファイル必須(相対パスでのインポートが基本)
  • スコープがモジュール単位(グローバル汚染しない)
<script type="module" src="main.js"></script>

● 実行タイミング比較まとめ

書き方実行タイミング補足
<script> その場で即時実行(ブロッキング) HTMLの解析が一時中断される
defer HTML解析完了後 記述順に実行される
type="module" HTML解析完了後 モジュールとして扱われる

● まとめ

  • defer:従来型スクリプトで、HTML後に実行したい場合に有効
  • type="module":モジュール構文 + deferと同様のタイミングで実行される
  • ✅ どちらもDOM構築完了後に安全にスクリプトが動作

次は、JavaScriptによってDOMを変更するタイミングについて詳しく見ていきましょう。


3. DOM変更と実行タイミングの注意点

解説: HTMLの解析が終わる前にJavaScriptで要素にアクセスしようとすると、nullが返ってしまうなどのエラーが起こります。
DOM操作を安全に行うには、「どのタイミングでDOMを触るべきか?」を理解しておくことが重要です。




● よくある失敗例:DOM構築前に要素取得

たとえば、次のようにHTMLの途中でスクリプトを実行すると、まだDOMが構築されていないためエラーになります。

<script>
  const btn = document.getElementById('myBtn');
  btn.addEventListener('click', () => {
    alert('クリックされました');
  });
</script>

<button id="myBtn">ボタン</button>

この場合、getElementByIdnull を返すため、addEventListener でエラーが発生します。


● 対策1:</body>の直前にscriptタグを置く

HTMLの最後、</body> の直前にスクリプトを置くことで、DOM構築完了後に実行されるようになります。

<button id="myBtn">ボタン</button>

<script>
  const btn = document.getElementById('myBtn');
  btn.addEventListener('click', () => {
    alert('クリックされました');
  });
</script>

この配置だけで、DOM構築後に実行されるため、エラーは起こりません。


● 対策2:defer属性やtype="module"属性を使う

セクション2で紹介したように、defer属性type="module"を使えば、DOM構築後にスクリプトが実行されるようになります。

<script src="main.js" defer></script>
<!-- または -->
<script type="module" src="main.js"></script>

この場合、スクリプトの中でDOMにアクセスしても安全です。


● 対策3:DOMContentLoadedイベントで包む

確実にDOM構築後に実行させたい場合は、DOMContentLoadedイベントの中で処理を書くのも有効です。

document.addEventListener('DOMContentLoaded', () => {
  const btn = document.getElementById('myBtn');
  btn.addEventListener('click', () => {
    alert('クリックされました');
  });
});

タイミングに迷ったら、まずこの書き方からスタートするのがおすすめです。


● まとめ

  • ✅ DOM構築前に要素を取得しようとすると null が返る
  • ✅ 対策として </body>直前にscriptタグを配置 / defer属性やtype="module" / DOMContentLoadedイベント のいずれかを使う
  • ✅ 初心者はまず DOMContentLoadedイベント を使えば安全

次は、DOM構築後に処理をわざと遅らせて実行する方法、つまり 非同期的なDOM操作 について見ていきましょう。


4. 非同期的にDOMを変更するタイミング(setTimeout / requestAnimationFrame)

解説: DOMの構築が完了していても、「いつDOMを変更するか」によって、描画のスムーズさやユーザー体験に影響が出ることがあります。
ここでは、処理をわざと後ろにずらすことでDOM操作を安全・スムーズに実行するテクニックを紹介します。




● setTimeout:処理を少しだけ遅らせて実行

まずは定番の setTimeout を使った方法です。処理を一定時間後に実行するため、DOMの反映や再構成のタイミングを意図的にコントロールできます。

setTimeout(() => {
  document.getElementById('message').textContent = 'こんにちは!';
}, 0);

上記のように 0ms を指定しても、JavaScriptのイベントループにより現在の処理が完了したあとに実行されます。


● requestAnimationFrame:次の描画フレームで実行

アニメーションやUI描画を最適化したい場合は、requestAnimationFrame の利用がおすすめです。
次の再描画タイミングに処理が実行されるため、ちらつきやレイアウトのズレを防ぎやすくなります。

requestAnimationFrame(() => {
  document.getElementById('box').style.transform = 'translateX(100px)';
});

この方法は、アニメーション開始のタイミングをブラウザに任せることで、最も適切な瞬間に実行される点が特徴です。


● setTimeoutとrequestAnimationFrameの違い

メソッド実行タイミング主な用途
setTimeout(fn, 0) 現在のタスクが終わった直後に実行キューへ追加 DOM更新を遅らせたいとき
requestAnimationFrame(fn) 次のリペイント前(最適な描画タイミング) アニメーションやUI描画の最適化

● まとめ

  • ✅ DOM構築後であっても、処理タイミングをずらすことで描画の安定性が高まる
  • setTimeout は「あとで実行したい」軽めの処理に向いている
  • requestAnimationFrame は描画に最適なタイミングで動くため、UI操作やアニメーションの制御に適している

次は、このようなタイミングの制御がなぜ重要なのかを、実際のDOMパフォーマンスやレイアウトへの影響という観点から深掘りしていきましょう。


5. DOM変更とパフォーマンス:レイアウト負荷を避けるコツ

解説: JavaScriptからDOMを頻繁に操作すると、ページの再描画(リペイント)や再レイアウト(リフロー)が多発し、パフォーマンスの低下を引き起こすことがあります。
ここでは、DOMを変更する際の負荷を抑えるコツや、避けたいパターンを具体例とともに紹介します。




● リフローとリペイントとは?

ブラウザは、DOMの変更に応じて以下のような処理を行います:

  • リフロー(Reflow):要素の位置やサイズを再計算して、レイアウトを再構築する処理
  • リペイント(Repaint):スタイル(色や背景など)の変更に伴う再描画処理

リフローは特にコストが高く、複雑なレイアウトではユーザーの体感速度に大きな影響を与えます。


● よくあるNG例:1つずつDOMを更新

以下は、繰り返しの中で毎回DOMを直接操作してしまっている例です。

const list = document.getElementById('list');
for (let i = 0; i < 100; i++) {
  const li = document.createElement('li');
  li.textContent = `項目${i}`;
  list.appendChild(li);  // ← 毎回リフローが発生する可能性あり
}

このように appendChild を繰り返すと、都度レイアウト再計算が走る可能性があり、描画がカクつく原因になります。


● 対策1:DocumentFragmentでまとめて挿入

DOM操作はできるだけ一括で行うのが理想です。以下のように DocumentFragment を使えば、リフローの回数を減らせます。

const list = document.getElementById('list');
const fragment = document.createDocumentFragment();

for (let i = 0; i < 100; i++) {
  const li = document.createElement('li');
  li.textContent = `項目${i}`;
  fragment.appendChild(li);
}

list.appendChild(fragment); // ← 一括で挿入

これにより、レイアウト再計算が一度で済み、よりスムーズに描画されます。


● 対策2:表示前に非表示にしてから更新

要素を大量に更新する場合は、一時的に非表示にしてから操作を行うと、不要なリフローを防げます。

const list = document.getElementById('list');
list.style.display = 'none';  // 一時的に非表示

// 項目を追加
for (let i = 0; i < 100; i++) {
  const li = document.createElement('li');
  li.textContent = `項目${i}`;
  list.appendChild(li);
}

list.style.display = '';      // 表示を戻す

単純なテクニックですが、大量操作時には意外と効果的です。


● 対策3:classListの活用で複数のstyle変更をまとめる

スタイルを1つずつ変更するのではなく、classList を使ってCSSクラスごと切り替えることで、スタイル適用の負荷を軽減できます。

// NG例:1つずつ style を変更
element.style.width = '200px';
element.style.height = '100px';
element.style.backgroundColor = 'blue';

// OK例:classListで一括適用
element.classList.add('active-style');

CSSの定義を活用することで、JavaScript側での操作を最小限にできます。


● まとめ

  • ✅ 頻繁なDOM変更はリフロー・リペイントを引き起こす
  • ✅ DocumentFragment や display:none でまとめて変更する工夫を
  • ✅ classListの活用や、不要な再計算の回避を意識しよう

DOMを操作する際は、タイミングだけでなくパフォーマンスにも気を配ることで、より快適なUIが実現できます。