В современных сайтах скрипты обычно «тяжелее», чем HTML: они весят больше, дольше обрабатываются.
Когда браузер загружает HTML и доходит до тега <script>...</script>
, он не может продолжать строить DOM. Он должен сначала выполнить скрипт. То же самое происходит и с внешними скриптами <script src="..."></script>
: браузер должен подождать, пока загрузится скрипт, выполнить его, и только затем обработать остальную страницу.
Это ведёт к двум важным проблемам:
- Скрипты не видят DOM-элементы ниже себя, поэтому к ним нельзя добавить обработчики и т.д.
- Если вверху страницы объёмный скрипт, он «блокирует» страницу. Пользователи не видят содержимое страницы, пока он не загрузится и не запустится:
<p>...содержимое перед скриптом...</p>
<script src="https://javascript.info/article/script-async-defer/long.js?speed=1"></script>
<!-- Это не отобразится, пока скрипт не загрузится -->
<p>...содержимое после скрипта...</p>
Конечно, есть пути, как это обойти. Например, мы можем поместить скрипт внизу страницы. Тогда он сможет видеть элементы над ним и не будет препятствовать отображению содержимого страницы:
<body>
...всё содержимое над скриптом...
<script src="https://javascript.info/article/script-async-defer/long.js?speed=1"></script>
</body>
Но это решение далеко от идеального. Например, браузер замечает скрипт (и может начать загружать его) только после того, как он полностью загрузил HTML-документ. В случае с длинными HTML-страницами это может создать заметную задержку.
Такие вещи незаметны людям, у кого очень быстрое соединение, но много кто в мире имеет медленное подключение к интернету или использует не такой хороший мобильный интернет.
К счастью, есть два атрибута тега <script>
, которые решают нашу проблему: defer
и async
.
defer
Атрибут defer
сообщает браузеру, что он должен продолжать обрабатывать страницу и загружать скрипт в фоновом режиме, а затем запустить этот скрипт, когда DOM дерево будет полностью построено.
Вот тот же пример, что и выше, но с defer
:
<p>...содержимое перед скриптом...</p>
<script defer src="https://javascript.info/article/script-async-defer/long.js?speed=1"></script>
<!-- отображается сразу же -->
<p>...содержимое после скрипта...</p>
- Скрипты с
defer
никогда не блокируют страницу. - Скрипты с
defer
всегда выполняются, когда дерево DOM готово, но до событияDOMContentLoaded
.
Следующий пример это показывает:
<p>...содержимое до скрипта...</p>
<script>
document.addEventListener('DOMContentLoaded', () => alert("Дерево DOM готово после скрипта с 'defer'!"));
</script>
<script defer src="https://javascript.info/article/script-async-defer/long.js?speed=1"></script> // (2)
<p>...содержимое после скрипта...</p>
- Содержимое страницы отобразится мгновенно.
- Событие
DOMContentLoaded
подождёт отложенный скрипт. Оно будет сгенерировано, только когда скрипт(2)
будет загружен и выполнен.
Отложенные с помощью defer
скрипты сохраняют порядок относительно друг друга, как и обычные скрипты.
Допустим, у нас есть два скрипта c defer
: small.js
и long.js
:
<script defer src="https://javascript.info/article/script-async-defer/long.js"></script>
<script defer src="https://javascript.info/article/script-async-defer/small.js"></script>
Браузеры сканируют страницу на предмет скриптов и загружают их параллельно в целях увеличения производительности. Поэтому и в примере выше оба скрипта скачиваются параллельно. small.js
скорее всего загрузится первым.
…Но defer
не только говорит браузеру «не блокировать рендеринг», он также обеспечивает правильную последовательность выполнения скриптов. Даже если small.js
загрузится первым, он будет ждать выполнения long.js
.
Это важно в тех случаях, когда нам сначала нужно загрузить JavaScript-библиотеку, а затем скрипт, который от неё зависит.
defer
предназначен только для внешних скриптовАтрибут defer
будет проигнорирован, если в теге <script>
нет src
.
async
Атрибут async
означает, что скрипт абсолютно независим:
- Страница не ждёт асинхронных скриптов, содержимое обрабатывается и отображается.
- Событие
DOMContentLoaded
и асинхронные скрипты не ждут друг друга:DOMContentLoaded
может произойти как до асинхронного скрипта (если асинхронный скрипт завершит загрузку после того, как страница будет готова),- …так и после асинхронного скрипта (если он короткий или уже содержится в HTTP-кеше)
- Остальные скрипты не ждут
async
, и скрипты casync
не ждут другие скрипты.
Так что если у нас есть несколько скриптов с async
, они могут выполняться в любом порядке. То, что первое загрузится – запустится в первую очередь:
<p>...содержимое перед скриптами...</p>
<script>
document.addEventListener('DOMContentLoaded', () => alert("DOM готов!"));
</script>
<script async src="https://javascript.info/article/script-async-defer/long.js"></script>
<script async src="https://javascript.info/article/script-async-defer/small.js"></script>
<p>...содержимое после скриптов...</p>
- Содержимое страницы отображается сразу же :
async
его не блокирует. DOMContentLoaded
может произойти как до, так и послеasync
, никаких гарантий нет.- Асинхронные скрипты не ждут друг друга. Меньший скрипт
small.js
идёт вторым, но скорее всего загрузится раньшеlong.js
, поэтому и запустится первым. То есть, скрипты выполняются в порядке загрузки.
Асинхронные скрипты очень полезны для добавления на страницу сторонних скриптов: счётчиков, рекламы и т.д. Они не зависят от наших скриптов, и мы тоже не должны ждать их:
<!-- Типичное подключение скрипта Google Analytics -->
<script async src="https://google-analytics.com/analytics.js"></script>
async
предназначен только для внешних скриптовКак и в случае с defer
, атрибут async
будет проигнорирован, если в теге <script>
нет src
.
Динамически загружаемые скрипты
Мы можем также добавить скрипт и динамически, с помощью 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
хорош для независимых скриптов, например счётчиков и рекламы, относительный порядок выполнения которых не играет роли.