Способы подключения скриптов в head: рекомендации и подводные камни

Раздел: Веб-разработка -> DOM

Размещение скриптов в HTML: выбор места и атрибутов

При подключении JavaScript к HTML-странице необходимо решить, где поместить тег <script> и какие атрибуты использовать. От этого зависят скорость загрузки страницы и корректность работы скриптов. Ниже рассмотрены основные варианты.

Рекомендуемый способ: скрипт с атрибутом defer в <head>

Этот способ загружает скрипт параллельно с HTML, но выполняет его только после полного разбора документа. Подходит для большинства случаев, когда скрипт обращается к DOM или зависит от других скриптов.

<!DOCTYPE html>
<html>
<head>
    <script src="app.js" defer></script>
</head>
<body>
    <div id="content"></div>
</body>
</html>

Dom js (dom в javascript)

Цель: обеспечить загрузку скрипта без блокировки рендеринга, с гарантией, что DOM уже создан. Используется для основных скриптов приложения.

Типичные проблемы и ошибки:

  • Забытый атрибут defer - скрипт выполняется сразу, блокируя парсинг.
  • Использование defer вместе с async - в таком случае async имеет приоритет, поведение становится неопределённым.
  • Скрипт пытается изменить DOM до его создания - ошибка отсутствия элементов. defer решает эту проблему.

Как сделать так, чтобы скрипт загружался параллельно, но выполнялся сразу после загрузки, не дожидаясь HTML?

Атрибут async

<script src="analytics.js" async></script>

Javascript document (объект document в javascript)

Цель: независимые скрипты (счётчики, реклама, трекеры), которые не обращаются к DOM и не зависят от других скриптов. Выполнение происходит сразу после загрузки файла, в любой момент.

Проблемы:

  • Невозможно гарантировать порядок выполнения нескольких асинхронных скриптов.
  • Скрипт может выполниться до полного формирования DOM, что вызовет ошибки при попытке доступа к элементам.
  • Не подходит для скриптов, использующих document.write.

Как гарантировать, что скрипт выполнится после загрузки всего DOM, но до загрузки изображений?

Размещение скрипта в конце <body>

<!DOCTYPE html>
<html>
<head>...</head>
<body>
    ...
    <script src="script.js"></script>
</body>
</html>

Js кнопка (создание и обработка кнопок в javascript)

Цель: старый, но надёжный способ. Скрипт выполняется после того, как парсер дошёл до него, то есть все элементы выше уже созданы. Подходит, если нужно гарантировать наличие DOM, но не хочется использовать defer (например, для совместимости с очень старыми браузерами).

Проблемы:

  • Скрипт всё ещё блокирует парсинг - оставшаяся часть body (если скрипт не в самом конце) не будет отображаться до его загрузки и выполнения.
  • Если скрипт большой, пользователь видит пустой экран до того, как он выполнится.
  • Не работает для скриптов, которые должны влиять на начальный рендеринг (например, критический CSS).

Как в <head> разместить модульные скрипты ES6 без блокировки?

Атрибут type="module"

<script type="module" src="main.js"></script>

Javascript nodes (javascript nodes (узлы))

Цель: скрипты, использующие import/export. Модули автоматически ведут себя как defer - загружаются параллельно, выполняются после полного разбора документа.

Проблемы:

  • Не поддерживаются в старых браузерах (IE 11, старые версии Safari).
  • Для внешних модулей требуется CORS, иначе браузер заблокирует загрузку.
  • В модулях document и window доступны, но this равен undefined (в классических скриптах this - глобальный объект).

Как выполнить скрипт, расположенный в <head>, только после полной готовности DOM?

Событие DOMContentLoaded внутри скрипта

<!DOCTYPE html>
<html>
<head>
    <script>
        document.addEventListener('DOMContentLoaded', function() {
            const el = document.getElementById('content');
            el.textContent = 'DOM готов';
        });
    </script>
</head>
<body>
    <div id="content"></div>
</body>
</html>

Javascript следующий (javascript: получение следующего элемента)

Цель: сохранение скрипта в <head> (например, для удобства структуры) при этом отложить его выполнение до построения DOM. Подходит для небольших встроенных скриптов.

Проблемы:

  • Сам файл скрипта (если он внешний) всё равно загружается и парсится немедленно, блокируя рендеринг. Это не решает проблему производительности.
  • Если скрипт использует async или defer, событие может быть уже отправлено до привязки обработчика - обработчик не сработает.

Как вставить небольшой скрипт непосредственно внутрь <head> без отдельного файла?

Встроенный (inline) скрипт

<head>
    <script>
        window.APP_CONFIG = { apiKey: '123' };
    </script>
</head>

Цель: быстрая инициализация, передача конфигурации, установка глобальных переменных до загрузки внешних скриптов. Удобно для небольших фрагментов.

Проблемы:

  • Не кэшируется браузером, что увеличивает размер HTML-страницы.
  • Затрудняет поддержку и тестирование.
  • При злоупотреблении ухудшает читаемость кода.
- Javascript head (размещение скриптов в html)
- Js содержимое (содержимое элементов javascript)

Примеры кода с пояснениями

Пример 1. Сравнение атрибутов async и defer

Два внешних скрипта: один с async, другой с defer. Порядок выполнения зависит от времени загрузки.

Пример
<!-- index.html -->
<head>
    <script src="async.js" async></script>
    <script src="defer.js" defer></script>
</head>
<body></body>

<!-- async.js -->
console.log('Async скрипт выполнен');

<!-- defer.js -->
console.log('Defer скрипт выполнен');
Возможный вывод (порядок не гарантирован):
Async скрипт выполнен
Defer скрипт выполнен

Если async.js загрузится быстрее - сначала его выполнение,
иначе первым может быть defer.js (но после полного парсинга HTML).

Пояснение: async выполняется сразу после загрузки, defer - только после обработки всего HTML. Для скриптов, которым нужен DOM, лучше defer.

Пример 2. Использование DOMContentLoaded в скрипте внутри head

Пример
<head>
    <script>
        document.addEventListener('DOMContentLoaded', function() {
            console.log('DOM полностью построен');
            document.body.style.backgroundColor = 'lightblue';
        });
        console.log('Скрипт загружен и выполняется (до готовности DOM)');
    </script>
</head>
<body>
    <p>Текст страницы</p>
</body>
Вывод в консоль:
Скрипт загружен и выполняется (до готовности DOM)
DOM полностью построен

Фон страницы станет голубым после события DOMContentLoaded.

Пояснение: первая строка выводится сразу, вторая - после того, как браузер разобрал HTML. Таким образом можно отложить манипуляции с DOM, не вынося скрипт в конец body.

Пример 3. Модульный скрипт type="module" с импортом

Пример
<!-- index.html -->
<head>
    <script type="module">
        import { greet } from './greet.js';
        greet('Мир');
    </script>
</head>

<!-- greet.js -->
export function greet(name) {
    console.log(`Привет, ${name}!`);
}
Вывод:
Привет, Мир!

(выполняется после полной загрузки HTML, как и defer)

Пояснение: модули автоматически откладываются. Импорт работает только при загрузке через HTTP(S) (не из локальных файлов без сервера).

Пример 4. Скрипт в конце body: обращение к DOM

Пример
<!DOCTYPE html>
<html>
<head>
    <title>Пример</title>
</head>
<body>
    <ul id="list">
        <li>Элемент 1</li>
        <li>Элемент 2</li>
    </ul>
    <script>
        const items = document.querySelectorAll('#list li');
        console.log('Количество пунктов:', items.length);
    </script>
</body>
</html>
Вывод:
Количество пунктов: 2

Пояснение: скрипт расположен после всех элементов списка, поэтому к моменту его выполнения DOM уже содержит эти элементы. Если бы скрипт был в head без defer, он бы не нашёл элементы.

Пример 5. Async скрипт для аналитики (не зависит от DOM)

Пример
<head>
    <script async src="https://example.com/tracker.js"></script>
</head>
<body>
    <p>Содержимое страницы</p>
</body>
Скрипт загрузится параллельно и выполнится, как только будет готов.
Например, трекер может отправлять данные о просмотре.
DOM страницы может быть ещё не полностью загружен - это нормально для аналитики.

Проблема: если трекер попытается изменить DOM (например, добавить рекламный блок),
элементы могут отсутствовать.

Пояснение: for independent scripts (analytics, ads) the async attribute is suitable. It does not guarantee the order, but it does not require access to the page elements.

Размещение скриптов в HTML - comments

En
Javascript head (javascript)