8.6 スクリプトの実行タイミング
DOM 操作を行う JavaScript は、操作対象の HTML 要素が存在していなければ正しく動きません。これまでの 演習で<script>タグを常に body の末尾に書いてきたのは、この理由からです。本節では、その仕組みを整理した上で、実務でよく使われる defer 属性と DOMContentLoaded イベントを学びます。
8.6.1 defer 属性と DOMContentLoaded イベント
なぜ body 末尾に書いてきたか
ブラウザは HTML を先頭から順番に読み込みます。<script>タグに差し掛かった時点で JavaScript を実行するため、まだ読み込まれていない HTML 要素(id や class を持つタグなど)を操作しようとするとエラーが発生します。body 末尾に<script>を配置することで、「その行に到達した時点ではページ上のすべての要素が読み込み済み」という状態を保証してきました。
defer 属性
実務では、パフォーマンスや保守性の観点から<script>タグを<head>内にまとめて書くケースがあります。その際に使うのが defer 属性です。
<script src="app.js" defer></script>
defer を付けると、「HTML の解析がすべて終わるまでスクリプトの実行を待つ」という指定になります。<head>内に書いても body 末尾に置いた場合と同じ動作が保証されます。
ポイント
- defer 属性を付けることで、<script>タグを内に書いても HTML の読み込み完了後にスクリプトが実行される。
DOMContentLoaded イベント
JavaScript コード側から同じ保証を行う方法が DOMContentLoaded イベントです。
document.addEventListener("DOMContentLoaded", () => {
// HTML の解析完了後に実行したい処理
});
DOMContentLoaded は、HTML の解析(パース)がすべて終わったタイミングで発火するイベントです。このリスナーの中に処理を書くことで、どこに<script>タグを置いても HTML 要素が確実に存在している状態で実行されます。defer 属性があれば DOMContentLoaded は必須ではありません。しかし、あえて両方書くことで、将来スクリプトの読み込み設定が変わっても壊れない堅牢なコードになります。第 12 章の統合演習でもこのパターンを使います。
ポイント
- DOMContentLoaded イベントのリスナー内に処理を書くことで、HTML の読み込み完了後にスクリプトを実行できる。
- defer 属性と DOMContentLoaded の組み合わせは、実務でよく使われる定番パターンである。
8.6.2 defer 属性を使ったプログラム
前の項で作成した dynamic-list.html を改造して、<script>タグを<head>内に移動させます。defer 属性とDOMContentLoaded を追加しても動作が変わらないことを確認しましょう。
ソースフォルダ:public/ch08
ファイル名:dynamic-list.js
➢ dynamic-list.html
<script>タグを body 末尾から内に移動し、defer 属性を追加します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>JavaScriptの練習: dynamic-list.jsを実行中</title>
<script src="dynamic-list.js" defer></script>
</head>
<body>
<h1>TODOリスト</h1>
<input type="text" id="todoInput" placeholder="タスクを入力">
<button id="addBtn">追加</button>
<ul id="todoList"></ul>
</body>
</html>
➢ dynamic-list.js
ファイル全体の処理を DOMContentLoaded のリスナーで囲みます。
// dynamic-list.js
document.addEventListener("DOMContentLoaded", () => {
// 要素を取得
const input = document.querySelector("#todoInput");
const addBtn = document.querySelector("#addBtn");
const todoList = document.querySelector("#todoList");
// タスクを追加する関数
function addTodo() {
const task = input.value;
// 入力が空の場合は処理しない
if (task === "") {
alert("タスクを入力してください");
return;
}
// 新しい li 要素を作成
const li = document.createElement("li");
li.textContent = task;
// 削除ボタンを作成
const deleteBtn = document.createElement("button");
deleteBtn.textContent = "削除";
// 削除ボタンがクリックされたときの処理
deleteBtn.addEventListener("click", function() {
todoList.removeChild(li);
});
// li に削除ボタンを追加
li.appendChild(deleteBtn);
// ul に li を追加
todoList.appendChild(li);
// 入力フィールドをクリア
input.value = "";
}
// 追加ボタンのクリックイベント
addBtn.addEventListener("click", addTodo);
// Enter キーでも追加できるようにする
input.addEventListener("keydown", function(event) {
if (event.key === "Enter") {
addTodo();
}
});
}); // DOMContentLoaded ここまで
実行結果
修正前と動作が変わらないことを確認してください。
変更したのは 2 点のみです。<script>タグが<head>内に移動し、defer 属性が付いたことで読み込みタイミングが保証されています。JavaScript 側は処理全体を DOMContentLoaded のリスナーで囲みました。内側の処理内容は前の項と完全に同じです。

解説
3 行目は、「HTML の解析が完了したら、この{ }内の処理を実行する」という命令です。6〜51 行目のコードはすべてこのリスナーの内側にあるため、#todoInput や#addBtn といった HTML 要素が確実に存在している状態で実行されます。
3: document.addEventListener("DOMContentLoaded", () => {
53 行目の});は、3 行目で開いた DOMContentLoaded リスナーを閉じています。インデントを 1 段深くしてすべての処理を内側に収めているため、コードの範囲を見落とさないよう注意しましょう。
53: }); // DOMContentLoaded ここまで
ポイント
- defer と DOMContentLoaded を使っても、処理の内容自体は変わらない。変わるのは「実行タイミングの制御方法」だけである。
- 第 12 章の統合演習では、この DOMContentLoaded リスナーの中に async を付けて Fetch API と組み合わせる。本節で構造を把握しておくと理解がスムーズになる。
