defer, async и куда ставить скрипт

хранение≈ 7 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

Браузер читает страницу сверху вниз, и каждый <script> — это развилка: остановить разбор и выполнить код прямо сейчас или отложить на потом. Атрибуты defer и async выбирают эту развилку за вас — надо только знать, чем они отличаются.

Разделы темы:·

Браузер читает HTML сверху вниз и по ходу строит DOM. Встретив <script src="...">, он останавливается: разбор замирает, файл качается, код выполняется — и только потом парсер идёт дальше. Причина историческая: скрипт вправе дописать разметку через document.write, и браузер не знает заранее, что окажется в потоке.

Отсюда два неприятных следствия. Скрипт в <head> задерживает появление страницы: пока он не выполнится, пользователь смотрит на пустое место. И скрипт видит только ту разметку, что находится выше него — всего, что ниже, для него ещё не существует.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

defer разделяет две вещи, которые в обычном скрипте склеены: загрузку и выполнение. Файл начинает качаться сразу, но параллельно разбору — парсер не тормозит ни на миллисекунду. Выполнится код тогда, когда DOM целиком построен, но ещё до события DOMContentLoaded.

Порядок при этом сохраняется: два defer-скрипта выполнятся в том порядке, в каком стоят в разметке, даже если второй скачался первым. Это ровно то, что нужно библиотеке и зависящему от неё коду. Важная деталь: defer имеет смысл только вместе с src — у инлайнового скрипта атрибут просто игнорируется.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Сами события и стадии readyState разбирает предыдущая статья — «DOMContentLoaded, load и порядок загрузки»; здесь важно только, где в этой схеме стоят атрибуты подключения. DOMContentLoaded срабатывает, когда HTML разобран и все defer-скрипты и модули отработали. Поэтому defer-скрипт всегда успевает на это событие подписаться, а код, добавленный на страницу позже, — уже нет: обработчик просто никогда не вызовется.

async-скрипт в эту схему не встроен: он может выполниться и до события, и после — как повезёт с сетью. А load ждёт вообще всё — картинки, шрифты, стили — и наступает заметно позже.

async тоже качает файл параллельно разбору, но выполняет его при первой возможности — как только файл приехал. Разбор при этом может прерваться на середине. Порядок в разметке не значит ничего: побеждает тот, кто раньше закончил загрузку.

Поэтому async годится ровно для одного класса файлов: самодостаточных, которым безразличны и другие скрипты, и состояние страницы, — счётчики, сбор ошибок, независимые виджеты. Библиотека и плагин к ней с async — это лотерея, которая выигрывается на быстрой машине разработчика и падает у пользователя.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

<script type="module"> отложен по умолчанию: defer дописывать не нужно, модуль и так качается параллельно, выполняется после разбора и в порядке разметки. Отличие от классики в том, что модуль ждёт свои import — браузер сначала соберёт весь граф зависимостей, потом выполнит его.

Отложенность распространяется и на инлайн: модуль без src тоже дожидается разбора, тогда как обычный инлайновый скрипт выполняется на месте. Атрибут async у модуля работает — со всеми последствиями непредсказуемого порядка.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

У скрипта, созданного через createElement, defer не действует, а async включён по умолчанию. Нужен порядок — выключайте async явно. Нужен модуль — берите import(): он возвращает промис с экспортами и сам заботится о том, чтобы один и тот же URL выполнился только раз.

У динамического скрипта есть onload и onerror. Второй обрабатывать обязательно: при обрыве сети иначе половина приложения молча не запустится, а в консоли не будет ни одной строки.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Правило простое. Всё, что работает со страницей, — 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 здесь недостаточно?

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Нужна одна строка рядом с s.src:

s.async = false;

Скрипт, созданный через createElement, по умолчанию имеет async === true — браузер выполнит его сразу после загрузки, поэтому маленький plugin.js обгоняет библиотеку. Атрибут defer тут не помогает: он влияет только на скрипты, которые парсер нашёл в разметке, а у динамического скрипта просто игнорируется.

С async = false браузер выстраивает такие скрипты в очередь: качаются они по-прежнему параллельно, а выполняются в порядке вставки в документ.