- Скрипты: async, defer
- defer
- async
- Динамически загружаемые скрипты
- Итого
- Порядок выполнения скриптов в HTML. Тег script и атрибуты async, defer, module, nomodule и src
- Картинка вместо тысячи слов
- Сравнение обычного с async, defer и async defer
- Сравнение type=module, type=text/javascript и nomodule
- Скрипты с type=module (также касается type=text/javascript)
- Сравнение встроенных (inline) и внешних скриптов
- Встроенные скрипты (без атрибута src)
- Внешние скрипты
Скрипты: async, defer
В современных сайтах скрипты обычно «тяжелее», чем HTML: они весят больше, дольше обрабатываются.
Когда браузер загружает HTML и доходит до тега , он не может продолжать строить DOM. Он должен сначала выполнить скрипт. То же самое происходит и с внешними скриптами : браузер должен подождать, пока загрузится скрипт, выполнить его, и только затем обработать остальную страницу.
Это ведёт к двум важным проблемам:
- Скрипты не видят DOM-элементы ниже себя, поэтому к ним нельзя добавить обработчики и т.д.
- Если вверху страницы объёмный скрипт, он «блокирует» страницу. Пользователи не видят содержимое страницы, пока он не загрузится и не запустится:
. содержимое перед скриптом.
. содержимое после скрипта.
Конечно, есть пути, как это обойти. Например, мы можем поместить скрипт внизу страницы. Тогда он сможет видеть элементы над ним и не будет препятствовать отображению содержимого страницы:
. всё содержимое над скриптом.
Но это решение далеко от идеального. Например, браузер замечает скрипт (и может начать загружать его) только после того, как он полностью загрузил HTML-документ. В случае с длинными HTML-страницами это может создать заметную задержку.
Такие вещи незаметны людям, у кого очень быстрое соединение, но много кто в мире имеет медленное подключение к интернету или использует не такой хороший мобильный интернет.
К счастью, есть два атрибута тега , которые решают нашу проблему: defer и async .
defer
Атрибут defer сообщает браузеру, что он должен продолжать обрабатывать страницу и загружать скрипт в фоновом режиме, а затем запустить этот скрипт, когда DOM дерево будет полностью построено.
Вот тот же пример, что и выше, но с defer :
. содержимое перед скриптом.
. содержимое после скрипта.
- Скрипты с defer никогда не блокируют страницу.
- Скрипты с defer всегда выполняются, когда дерево DOM готово, но до события DOMContentLoaded .
Следующий пример это показывает:
. содержимое до скрипта.
// (2) . содержимое после скрипта.
- Содержимое страницы отобразится мгновенно.
- Событие DOMContentLoaded подождёт отложенный скрипт. Оно будет сгенерировано, только когда скрипт (2) будет загружен и выполнен.
Отложенные с помощью defer скрипты сохраняют порядок относительно друг друга, как и обычные скрипты.
Поэтому, если сначала загружается большой скрипт, а затем меньшего размера, то последний будет ждать.
Браузеры сканируют страницу на предмет скриптов и загружают их параллельно в целях увеличения производительности. Поэтому и в примере выше оба скрипта скачиваются параллельно. small.js скорее всего загрузится первым.
Но спецификация требует последовательного выполнения скриптов согласно порядку в документе, поэтому он подождёт выполнения long.js .
Атрибут defer будет проигнорирован, если в теге нет src .
async
Атрибут async означает, что скрипт абсолютно независим:
- Страница не ждёт асинхронных скриптов, содержимое обрабатывается и отображается.
- Событие DOMContentLoaded и асинхронные скрипты не ждут друг друга:
- DOMContentLoaded может произойти как до асинхронного скрипта (если асинхронный скрипт завершит загрузку после того, как страница будет готова),
- …так и после асинхронного скрипта (если он короткий или уже содержится в HTTP-кеше)
Так что если у нас есть несколько скриптов с async , они могут выполняться в любом порядке. То, что первое загрузится – запустится в первую очередь:
. содержимое перед скриптами.
. содержимое после скриптов.
- Содержимое страницы отображается сразу же : async его не блокирует.
- DOMContentLoaded может произойти как до, так и после async , никаких гарантий нет.
- Асинхронные скрипты не ждут друг друга. Меньший скрипт small.js идёт вторым, но скорее всего загрузится раньше long.js , поэтому и запустится первым. То есть, скрипты выполняются в порядке загрузки.
Асинхронные скрипты очень полезны для добавления на страницу сторонних скриптов: счётчиков, рекламы и т.д. Они не зависят от наших скриптов, и мы тоже не должны ждать их:
Динамически загружаемые скрипты
Мы можем также добавить скрипт и динамически, с помощью JavaScript:
let script = document.createElement('script'); script.src = "/article/script-async-defer/long.js"; document.body.append(script); // (*)
Скрипт начнёт загружаться, как только он будет добавлен в документ (*) .
Динамически загружаемые скрипты по умолчанию ведут себя как «async».
- Они никого не ждут, и их никто не ждёт.
- Скрипт, который загружается первым – запускается первым (в порядке загрузки).
Мы можем изменить относительный порядок скриптов с «первый загрузился – первый выполнился» на порядок, в котором они идут в документе (как в обычных скриптах) с помощью явной установки свойства async в false :
let script = document.createElement('script'); script.src = "/article/script-async-defer/long.js"; script.async = false; document.body.append(script);
Например, здесь мы добавляем два скрипта. Без script.async=false они запускались бы в порядке загрузки ( small.js скорее всего запустился бы раньше). Но с этим флагом порядок будет как в документе:
function loadScript(src) < let script = document.createElement('script'); script.src = src; script.async = false; document.body.append(script); >// long.js запускается первым, так как async=false loadScript("/article/script-async-defer/long.js"); loadScript("/article/script-async-defer/small.js");
Итого
У async и defer есть кое-что общее: они не блокируют отрисовку страницы. Так что пользователь может просмотреть содержимое страницы и ознакомиться с ней сразу же.
Но есть и значимые различия:
Порядок DOMContentLoaded async Порядок загрузки (кто загрузится первым, тот и сработает). Не имеет значения. Может загрузиться и выполниться до того, как страница полностью загрузится. Такое случается, если скрипты маленькие или хранятся в кеше, а документ достаточно большой. defer Порядок документа (как расположены в документе). Выполняется после того, как документ загружен и обработан (ждёт), непосредственно перед DOMContentLoaded . Пожалуйста, помните, что когда вы используете defer , страница видна до того, как скрипт загрузится.
Пользователь может знакомиться с содержимым страницы, читать её, но графические компоненты пока отключены.
Поэтому обязательно должна быть индикация загрузки, нерабочие кнопки – отключены с помощью CSS или другим образом. Чтобы пользователь явно видел, что уже готово, а что пока нет.
На практике defer используется для скриптов, которым требуется доступ ко всему DOM и/или важен их относительный порядок выполнения.
А async хорош для независимых скриптов, например счётчиков и рекламы, относительный порядок выполнения которых не играет роли.
Порядок выполнения скриптов в HTML. Тег script и атрибуты async, defer, module, nomodule и src
С добавлением в JavaScript ES-модулей появилось не менее 24 способов подгрузить скрипты: с атрибутом src и без него; с async или без; defer или нет; type=module и nomodule. Все они немножко отличаются друг от друга.
В этой статье сравним, как встроенные в HTML тэги обрабатываются в зависимости от набора атрибутов.
Картинка вместо тысячи слов
Мы видим, что async используется в legacy-скриптах, когда нужно выполнить их пораньше, а module — наоборот, чтобы задержать выполнение до подходящего момента (модульные скрипты по умолчанию обладают атрибутом defer).
Шпаргалку сохранили, а теперь рассмотрим каждый из вариантов подробней.
Сравнение обычного с async, defer и async defer
Async и defer полностью поддерживаются и, как уже говорилось, интуитивно понятная разница между ними заключается в том, что скрипты с async выполняются сразу. Они не ждут окончания парсинга HTML, полного формирования DOM, а также подгрузки остальных скриптов.
Обычные немодульные
- Приостанавливают парсинг HTML.
- Сразу подгружаются, парсятся и выполняются.
- Гарантируют порядок выполнения относительно других обычных немодульных скриптов.
- Блокируют событие DOMContentLoaded.
- Учитывая всё вышесказанное, такие скрипты не подходят для некритичного кода, поскольку замедляют рендеринг и, как следствие, загрузку динамических веб-приложений.
- Для встроенных (inline) немодульных скриптов defer игнорируется, и код выполняются сразу. Если defer прям сильно нужен, можно воспользоваться обходным путём с участием base64.
- Для встроенных скриптов с указанием type=”module” defer применяется по умолчанию.
- Подгружаются без остановки HTML-парсера.
- Гарантируют порядок выполнения относительно других defer-скриптов (если они внешние — с атрибутом src). Криво работают в IE9.
- Выполняются после окончания парсинга DOM (но перед срабатыванием DOMContentLoaded).
- Блокируют событие DOMContentLoaded (только если скрипт не async defer).
- Для встроенных (inline) немодульных скриптов async игнорируется.
- Для встроенных модульных скриптов async поддерживается.
- Подгружаются без остановки HTML-парсера.
- Выполняются без очереди.
- Не гарантируют порядок выполнения относительно других скриптов с async (также касается модульных скриптов с async).
- Не ждут окончания парсинга HTML. Могут прервать построение DOM (в частности, когда он достаётся из кэша браузера).
- Блокируют событие load (но не DOMContentLoad).
- Не поддерживаются в IE9.
Воспринимаются как async. В древних брузерных движках, которые не поддерживают async (IE9), работает так же, как defer.
Сравнение type=module, type=text/javascript и nomodule
Скрипты с type=module (также касается type=text/javascript)
- Предполагают defer (также для встроенных скриптов, в отличие от немодульных скриптов).
- Исходя из этого, гарантируют порядок выполнения относительно всех модульных скриптов, не использующих async (как встроенных, так и внешних).
- Выполняются только раз, даже если скрипты с одинаковым src подгружаются несколько раз.
- Могут использовать import для объявления зависимости с другими модульными скриптами (одна из причин, почему модули предполагают использование defer).
- Подвергаются проверке CORS (в модулях из разных источников потребуется указать Access-Control-Allow-Origin: [источники]).
- Не выполняются браузерами, которые не поддерживают модульные скрипты. Однако они всё ещё, по видимому, подгружаются в IE11, Firefox 52 ESR и т.д.
Не выполняются браузерами, которые не поддерживают . Однако, даже некоторые современные браузеры по ошибке подтягивают их (например Safari 10.3, но существует способ это исправить).
Сравнение встроенных (inline) и внешних скриптов
Встроенные скрипты (без атрибута src)
- Для немодульных скриптов async и defer игнорируются.
- Блокируют HTML-парсеры и построение DOM, так как выполняются сразу после загрузки.
- Встроенные модульные скрипты предполагают defer. Также поддерживают async.
- Не кэшируются браузерами.
Внешние скрипты
Кэшируются браузерами (при условии подходящих заголовков в ответе от сервера), поэтому могут использоваться в будущем без повторной подгрузки из сети.