defer, async и куда ставить скрипт
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
Браузер читает страницу сверху вниз, и каждый <script> — это развилка: остановить разбор и выполнить код прямо сейчас или отложить на потом. Атрибуты defer и async выбирают эту развилку за вас — надо только знать, чем они отличаются.
Браузер читает HTML сверху вниз и по ходу строит DOM. Встретив <script src="...">, он останавливается: разбор замирает, файл качается, код выполняется — и только потом парсер идёт дальше. Причина историческая: скрипт вправе дописать разметку через document.write, и браузер не знает заранее, что окажется в потоке.
Отсюда два неприятных следствия. Скрипт в <head> задерживает появление страницы: пока он не выполнится, пользователь смотрит на пустое место. И скрипт видит только ту разметку, что находится выше него — всего, что ниже, для него ещё не существует.
defer разделяет две вещи, которые в обычном скрипте склеены: загрузку и выполнение. Файл начинает качаться сразу, но параллельно разбору — парсер не тормозит ни на миллисекунду. Выполнится код тогда, когда DOM целиком построен, но ещё до события DOMContentLoaded.
Порядок при этом сохраняется: два defer-скрипта выполнятся в том порядке, в каком стоят в разметке, даже если второй скачался первым. Это ровно то, что нужно библиотеке и зависящему от неё коду. Важная деталь: defer имеет смысл только вместе с src — у инлайнового скрипта атрибут просто игнорируется.
Сами события и стадии readyState разбирает предыдущая статья — «DOMContentLoaded, load и порядок загрузки»; здесь важно только, где в этой схеме стоят атрибуты подключения. DOMContentLoaded срабатывает, когда HTML разобран и все defer-скрипты и модули отработали. Поэтому defer-скрипт всегда успевает на это событие подписаться, а код, добавленный на страницу позже, — уже нет: обработчик просто никогда не вызовется.
async-скрипт в эту схему не встроен: он может выполниться и до события, и после — как повезёт с сетью. А load ждёт вообще всё — картинки, шрифты, стили — и наступает заметно позже.
async тоже качает файл параллельно разбору, но выполняет его при первой возможности — как только файл приехал. Разбор при этом может прерваться на середине. Порядок в разметке не значит ничего: побеждает тот, кто раньше закончил загрузку.
Поэтому async годится ровно для одного класса файлов: самодостаточных, которым безразличны и другие скрипты, и состояние страницы, — счётчики, сбор ошибок, независимые виджеты. Библиотека и плагин к ней с async — это лотерея, которая выигрывается на быстрой машине разработчика и падает у пользователя.
<script type="module"> отложен по умолчанию: defer дописывать не нужно, модуль и так качается параллельно, выполняется после разбора и в порядке разметки. Отличие от классики в том, что модуль ждёт свои import — браузер сначала соберёт весь граф зависимостей, потом выполнит его.
Отложенность распространяется и на инлайн: модуль без src тоже дожидается разбора, тогда как обычный инлайновый скрипт выполняется на месте. Атрибут async у модуля работает — со всеми последствиями непредсказуемого порядка.
У скрипта, созданного через createElement, defer не действует, а async включён по умолчанию. Нужен порядок — выключайте async явно. Нужен модуль — берите import(): он возвращает промис с экспортами и сам заботится о том, чтобы один и тот же URL выполнился только раз.
У динамического скрипта есть onload и onerror. Второй обрабатывать обязательно: при обрыве сети иначе половина приложения молча не запустится, а в консоли не будет ни одной строки.
Правило простое. Всё, что работает со страницей, — defer или модуль в <head>: браузер узнаёт про файл максимально рано, а выполняет максимально удобно. Независимые счётчики — async. Скрипт в конце <body> тоже не блокирует разметку, но начинает качаться позже всех, так что это скорее наследие, чем выбор.
| Как подключён | Когда качается | Когда выполняется | Порядок |
|---|---|---|---|
| <script src> | во время разбора, разбор стоит | сразу после загрузки | по документу |
| <script defer src> | параллельно разбору | после разбора, до DOMContentLoaded | по документу |
| <script async src> | параллельно разбору | как только скачался | какой получится |
| <script type=module src> | параллельно разбору | после разбора, до DOMContentLoaded | по документу, после импортов |
| <script type=module async> | параллельно разбору | как только собран граф импортов | какой получится |
Отдельная ловушка — считать, что «поставил defer, и порядок в безопасности». Он в безопасности только для скриптов, которые браузер нашёл в разметке. Всё, что вы добавляете на страницу из JavaScript, по умолчанию ведёт себя как async.
Крошечный кусок кода, нужный до первой отрисовки — тема оформления, флаг эксперимента, — иногда честнее вставить прямо в head инлайном. Миллисекунда блокировки дешевле лишнего запроса и мигания страницы.
- Обычный <script src> останавливает разбор страницы: браузер качает файл, выполняет код и только потом продолжает строить DOM — поэтому такой скрипт видит лишь разметку выше себя.
- defer качает параллельно разбору, а выполняет после него и строго в порядке разметки, до DOMContentLoaded. Это выбор по умолчанию для кода, который работает со страницей.
- async качает параллельно, но выполняет сразу после загрузки: порядок не гарантирован. Годится только для самодостаточных файлов вроде счётчиков.
- type=module отложен по умолчанию, как defer, плюс своя область видимости, strict mode и одно выполнение на URL; async на модуле тоже работает.
- Атрибуты действуют только вместе с src. У скрипта, созданного из JavaScript, async уже включён — отключайте его явно, если нужен порядок.
1/3В head подряд стоят <script async src="lib.js"> и <script async src="plugin.js">, причём plugin использует функции из lib. Что будет?
2/3const s = document.createElement("script"); s.defer = true; console.log(s.async); — что в консоли?
3/3Чем <script type="module" src> отличается от <script defer src>?
Что здесь гарантировано
В <head> стоит такая разметка:
<script async src="analytics.js"></script>
<script defer src="lib.js"></script>
<script defer src="app.js"></script>
Файл app.js вызывает функции из lib.js. Ответьте на три вопроса: 1) может ли app.js выполниться раньше lib.js; 2) может ли analytics.js обогнать обоих; 3) успеют ли defer-скрипты подписаться на DOMContentLoaded и увидят ли они всю разметку страницы?
1. Нет. defer сохраняет порядок документа: lib.js выполнится первым, даже если app.js скачался раньше. Загружаются они параллельно, а выполняются строго по очереди.
2. Да, в любой момент. async-скрипт выполняется сразу после загрузки и ни с кем не синхронизируется — он может отработать и до, и между, и после defer-скриптов. Именно поэтому в analytics.js не должно быть ничего, что нужно другим файлам.
3. Да. defer-скрипты выполняются после полного разбора HTML и до DOMContentLoaded: весь DOM им доступен, и подписаться на событие они ещё успевают. У analytics.js такой гарантии нет.
Верните порядок динамическим скриптам
Скрипты создаются в коде и добавляются в <head>, но plugin.js иногда выполняется раньше lib.js и падает. Что дописать в тело map, чтобы порядок стал гарантированным, — и почему одного s.defer = true здесь недостаточно?
Нужна одна строка рядом с s.src:
s.async = false;
Скрипт, созданный через createElement, по умолчанию имеет async === true — браузер выполнит его сразу после загрузки, поэтому маленький plugin.js обгоняет библиотеку. Атрибут defer тут не помогает: он влияет только на скрипты, которые парсер нашёл в разметке, а у динамического скрипта просто игнорируется.
С async = false браузер выстраивает такие скрипты в очередь: качаются они по-прежнему параллельно, а выполняются в порядке вставки в документ.