Сразу отвечу: тег script лучше ставить в конце body - так загружается контент, а потом уже скрипты. Но всё не так просто: в зависимости от задачи и требований к скорости загрузки могут пригодиться варианты в head, атрибуты async и defer, а также модульные скрипты.
Что такое тег script и какие задачи он решает
script - это HTML‑элемент, позволяющий подключать или писать JavaScript‑код, который будет выполнен в браузере пользователя. Он может загружать внешний файл через атрибут src или содержать встроенный код между открывающим и закрывающим тегами. Скрипты управляют интерактивностью, отправкой форм, анимацией, динамической подгрузкой данных и многим другим.
Классические места размещения скриптов
- Внутри
head- скрипт загружается до отрисовки любой части страницы. Это удобно, когда нужен ранний доступ к глобальным переменным или функции, вызываемые сразу при парсинге HTML. - В конце
body- самый распространённый вариант. Браузер сначала обработает весь контент, а потом выполнит скрипт, что ускоряет визуальную загрузку.
Размещение в head без атрибутов заставляет браузер блокировать рендеринг до полной загрузки и выполнения скрипта. Поэтому в современных проектах стараются избегать такого поведения, используя async или defer.
Атрибут async: загрузка без блокировки
Когда к тегу script добавлен async, браузер начинает скачивать файл параллельно с построением DOM‑дерева. Как только скрипт готов, он сразу же исполняется, даже если страница ещё не полностью построена. Это полезно для небольших аналитических или рекламных скриптов, которые не влияют на работу основного контента.
Недостаток - отсутствие гарантии порядка выполнения, если их несколько. Если один скрипт зависит от другого, async может привести к ошибкам.
Атрибут defer: отложенный запуск
С defer браузер также скачивает файл параллельно, но откладывает исполнение до полного построения DOM и события DOMContentLoaded. Скрипты с defer сохраняют порядок, прописанный в документе, что делает их безопасным выбором для большинства внешних библиотек.
Важно: атрибут defer работает только с внешними скриптами (src), встроенный код игнорирует его.
Модульные скрипты (type="module")
С появлением ES‑модулей в браузерах появился новый способ организации кода. Тег script с атрибутом type="module" автоматически получает поведение defer - скрипт загружается асинхронно и исполняется после построения DOM. Кроме того, модули поддерживают import и export, позволяют разбивать код на файлы без глобального пространства имён.
Если ваш проект уже использует сборщики типа Webpack, Rollup или Vite, модульные скрипты - естественное продолжение.
Влияние места размещения на производительность
Google Lighthouse и PageSpeed Insights измеряют метрики, такие как First Contentful Paint (FCP) и Time to Interactive (TTI). Блокирующие скрипты в head резко ухудшают эти показатели, потому что пользователь видит пустой экран дольше.
Расположение в конце body или использование defer/async сокращает время до первого отрисованного элемента. По данным Web.dev, правильное применение defer может ускорить TTI на 30‑40 %.
Проверка и отладка размещения скриптов
- Откройте DevTools → Network. Убедитесь, что файлы скриптов отмечены как «async» или «defer», если вы их использовали.
- Перейдите во вкладку Performance, запустите запись и посмотрите, когда срабатывает событие
DOMContentLoadedи какой код его задерживает. - Если FCP > 2 с, попробуйте переместить тяжелый скрипт в конец
bodyили добавитьdefer.
Чек‑лист: где разместить script
- Для аналитики, рекламы, небольших виджетов -
asyncвheadили в концеbody. - Для основных библиотек (jQuery, React, Vue) -
deferвheadили обычныйscriptв концеbody. - Для модульного кода -
type="module", размещать вheadбез опасений блокировки. - Если скрипт манипулирует DOM сразу после загрузки - ставьте его в конец
bodyбез атрибутов. - Избегайте инлайн‑скриптов в
head, если только они не критичны для начального рендеринга.
Типичные ошибки и как их исправлять
| Ошибка | Последствия | Решение |
|---|---|---|
Блокирующий script в head | Значительно замедленный рендеринг, плохие метрики | Добавить defer или перенести в конец body |
Несколько async‑скриптов с зависимостями | Непредсказуемый порядок выполнения, ошибки «undefinded» | Перейти на defer или объединить в один модуль |
Встроенный скрипт без defer в head | Блокировка парсинга, срыв CSS‑адаптивности | Переместить код в конец body или вынести в отдельный файл с defer |
Отсутствие атрибута type="module" у модульных файлов | Браузер считает их обычными скриптами, ломается импорт | Указать type="module" и использовать defer по умолчанию |
Практический пример
Допустим, у вас на странице три скрипта:
- Библиотека
lodash.js(независимая утилита). - Скрипт
analytics.js(отправка статистики). - Ваш главный
app.js, который используетlodashи зависит от DOM.
Оптимальный шаблон HTML будет выглядеть так:
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Пример оптимального размещения скриптов</title>
<script src="/js/lodash.js" defer></script>
<script src="/js/analytics.js" async></script>
</head>
<body>
<!-- Содержимое страницы -->
<script src="/js/app.js" defer></script>
</body>
</html>
Здесь lodash и app получат отложенный запуск после парсинга DOM, а аналитика загрузится независимо и не будет мешать пользователю.
Заключительные мысли
Выбор места для тег script зависит от того, насколько критичен код для начального рендеринга и есть ли у него зависимости. Правильное использование async, defer и модульных скриптов помогает сохранить быстрый First Contentful Paint и улучшить пользовательский опыт.
Можно ли ставить несколько скриптов с атрибутом defer?
Да, браузер выполнит их в том порядке, в каком они указаны в документе. Это удобно для библиотек, которые зависят друг от друга.
Чем отличается defer от async?
async загружает скрипт параллельно и исполняет его сразу после загрузки, без ожидания построения DOM. defer тоже загружает параллельно, но откладывает исполнение до события DOMContentLoaded и сохраняет порядок.
Нужен ли defer для модульных скриптов?
Нет. При указании type="module" скрипт автоматически получает поведение defer, поэтому отдельный атрибут не нужен.
Как проверить, блокирует ли скрипт рендеринг?
Откройте DevTools → Network, включите колонку «Blocking» и смотрите, какие запросы имеют статус «blocking». Также обратите внимание на метрику «Render‑Blocking Resources» в Lighthouse.
Можно ли использовать async и defer одновременно?
Нет, эти атрибуты взаимоисключающие. Если указать оба, браузер выберет один из них (обычно defer), но это считается плохой практикой.