Learn JavaScript

Подключение script - где ставить, defer и порядок выполнения

Скрипт можно запустить в консоли DevTools, но для реальной страницы JavaScript подключается через тег <script>. Важно понимать, где он стоит в HTML и как браузер выполняет код - от этого зависит, найдутся ли элементы на странице и в каком порядке сработают файлы.

Базовый вариант - script без атрибутов

Простейшее подключение внешнего файла:

<script src="/js/app.js"></script>

Что происходит по умолчанию:

  1. Браузер читает HTML сверху вниз.
  2. Когда встречается <script src="...">, загрузка HTML останавливается.
  3. Браузер скачивает файл скрипта.
  4. Скрипт выполняется.
  5. Только потом браузер продолжает разбор HTML.

Это называется “blocking” (блокирующее подключение).

Почему так часто появляются ошибки с DOM

Если скрипт стоит в <head> и пытается найти элементы, которых еще нет в HTML, можно получить null:

<head>
  <script src="/js/app.js"></script>
</head>
<body>
  <button id="btn">OK</button>
</body>
// app.js
const btn = document.querySelector("#btn");
console.log(btn); // null

Причина простая - #btn еще не был разобран браузером, когда скрипт выполнился.

Где ставить script

Есть два распространенных места: в <head> или в конце <body>. Оба работают, но с разными правилами.

В конце body

Классический подход - поставить script перед закрывающим </body>:

<body>
  <button id="btn">OK</button>

  <script src="/js/app.js"></script>
</body>

Плюсы:

  • почти всегда DOM уже построен
  • не нужен defer

Минусы:

  • когда проектов много, трудно поддерживать порядок подключений
  • скрипты “размазаны” по разметке (если их много)

В head с defer

Современный удобный подход - держать подключения в <head>, но использовать defer:

<head>
  <script src="/js/app.js" defer></script>
</head>

defer - главный атрибут для большинства страниц

defer меняет поведение загрузки и выполнения:

  • файл скачивается параллельно с разбором HTML
  • выполнение откладывается до момента, когда HTML уже разобран
  • несколько скриптов с defer выполняются в том порядке, в котором идут в документе

Пример с несколькими файлами:

<head>
  <script src="/js/lib.js" defer></script>
  <script src="/js/app.js" defer></script>
</head>

Порядок будет таким:

  1. HTML разбирается
  2. Скрипты скачиваются параллельно
  3. Выполнится lib.js
  4. Выполнится app.js

Это важно, если app.js зависит от кода из lib.js.

async - когда порядок не важен

async тоже позволяет скачивать файл параллельно, но есть ключевые отличия:

  • скрипт выполнится сразу после загрузки, не дожидаясь конца разбора HTML
  • порядок между несколькими async не гарантируется
<head>
  <script src="/js/metrics.js" async></script>
</head>

Типичное применение - аналитика, счетчики, виджеты, где не важен порядок и нет зависимости от DOM в момент выполнения.

Inline script - код прямо в HTML

Иногда скрипт пишут прямо внутри тега <script>:

<script>
  console.log("inline script");
</script>

Правила те же: если такой блок стоит до элементов DOM, код выполнится раньше появления элементов.

Часто inline используют для небольших настроек, например передачи переменной в JS:

<script>
  window.APP_VERSION = "1.0.0";
</script>
<script src="/js/app.js" defer></script>

Здесь важно учитывать порядок: app.js должен видеть window.APP_VERSION.

Важный момент - DOMContentLoaded

Событие DOMContentLoaded срабатывает, когда HTML разобран и DOM построен (картинки и стили могут еще догружаться).

Если script подключен без defer и стоит в <head>, можно подождать DOM так:

document.addEventListener("DOMContentLoaded", () => {
  const btn = document.querySelector("#btn");
  console.log(btn);
});

Но в большинстве случаев проще и чище подключать файл с defer.

Порядок выполнения - короткая шпаргалка

  • <script src="..." > без атрибутов:
    • блокирует разбор HTML
    • выполняется сразу при встрече
  • <script src="..." defer>:
    • не блокирует разбор HTML
    • выполняется после разбора HTML
    • сохраняет порядок между defer-скриптами
  • <script src="..." async>:
    • не блокирует разбор HTML
    • выполняется сразу после загрузки
    • порядок между async-скриптами не гарантируется

Частые проблемы и быстрые проверки

Скрипт не запускается

  • Проверить путь в src (опечатка, неверная папка).
  • Открыть DevTools -> Network и убедиться, что файл загружается без 404.
  • Проверить Console на ошибки.

Код видит null вместо элементов

  • Скрипт выполняется слишком рано.
  • Решения: перенести в конец <body>, добавить defer, или обернуть код в DOMContentLoaded.

Не работает зависимость между файлами

  • Скрипты должны выполняться в правильном порядке.
  • Для нескольких файлов чаще всего нужен defer и корректный порядок в HTML.

Короткий итог

Для обычных учебных и прикладных страниц самый удобный и безопасный вариант - подключать скрипты в <head> с defer. Это дает корректный порядок выполнения, не блокирует разбор HTML и позволяет коду работать с DOM без дополнительных обходных конструкций.