На сучасних вебсайтах скрипти часто “важчі” за 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
повідомляє браузеру, що йому не треба чекати на скрипт. Замість цього браузер продовжить обробляти HTML, будувати DOM. Скрипт завантажується “у фоновому режимі”, а потім запускається, коли 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>
<p>...вміст після скриптів...</p>
- Вміст сторінки з’являється негайно.
- Обробник події
DOMContentLoaded
чекає на відкладений скрипт. Він запускається лише тоді, коли скрипт буде завантажено та виконано.
Відкладені скрипти зберігають відносний порядок, як і звичайні скрипти.
Скажімо, у нас є два відкладених скрипти: long.js
, а потім small.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
дещо схожий на defer
. Він також робить скрипт неблокуючим. Але він має важливі відмінності в поведінці.
Атрибут async
означає, що скрипт повністю незалежний:
- Браузер не блокує
async
скрипти (якdefer
). - Інші скрипти не чекають
async
скриптів, аasync
скрипти не чекають їх. DOMContentLoaded
та асинхронні скрипти не чекають один одного:DOMContentLoaded
може відбуватися як перед асинхронним скриптом (якщо асинхронний скрипт закінчує завантаження після того, як сторінка готова)- …або після асинхронного скрипту (якщо асинхронний скрипт короткий або був у HTTP-кеші)
Іншими словами, async
скрипти завантажуються у фоновому режимі та запускаються по готовності. DOM та інші скрипти їх не чекають, і вони нічого не чекають. Повністю незалежний скрипт, який запускається, коли завантажений. Настільки просто, наскільки можливо, чи не так?
Ось приклад, подібний до того, що ми бачили з defer
: два скрипти long.js
і small.js
, але тепер із async
замість defer
.
Вони не чекають один одного. Все, що завантажується першим (ймовірно, small.js
) – запускається першим:
<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
, тому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”.
Тобто:
- Вони нічого не чекають, їх нічого не чекає.
- Скрипт, який завантажується першим – запускається першим (в порядку “першим завантажився – першим виконався”).
Це можна змінити, якщо ми явно встановимо script.async=false
. Тоді скрипти будуть виконуватися в порядку розміщення в документі, як при defer
.
У цьому прикладі функція loadScript(src)
додає скрипт, а також встановлює для async
значення false
.
Тому long.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");
Без script.async=false
скрипти виконуватимуться за замовчуванням, в порядку “першим завантажився – першим виконався” (мабуть, першим буде small.js
).
Знову ж таки, як і у випадку з defer
, порядок має значення, якщо ми хочемо завантажити бібліотеку, а потім інший скрипт, який залежить від неї.
Підсумки
І async
, і defer
мають одну спільну рису: завантаження таких скриптів не блокує відтворення сторінки. Таким чином користувач може прочитати вміст сторінки та одразу ознайомитися з нею.
Але між ними є й істотні відмінності:
Порядок | DOMContentLoaded |
|
---|---|---|
async |
Порядок завантаження. Їхній порядок в документі не має значення: що завантажується першим – запускається першим | Не має значення. Може завантажуватися та виконуватися, поки документ ще не завантажено повністю. Це трапляється, якщо скрипти невеликі або кешовані, а документ достатньо великий. |
defer |
Порядок документа (як вони розміщені в документі). | Виконуються після завантаження та аналізу документа (за потреби вони чекають), безпосередньо перед DOMContentLoaded . |
На практиці defer
використовується для скриптів, яким потрібен весь DOM, і/або важливий їх відносний порядок виконання.
А async
використовується для незалежних скриптів, таких як лічильники або реклама. І їх відносний порядок виконання не має значення.
Зверніть увагу: якщо ви використовуєте defer
або async
, користувач побачить сторінку перед завантаженням скрипту.
У такому випадку деякі графічні компоненти, ймовірно, ще не ініціалізовано.
Не забудьте поставити індикацію “завантаження” та відключити кнопки, які ще не працюють. Нехай користувач чітко бачить, що він може робити на сторінці, а що ще готується.
Коментарі
<code>
, для кількох рядків – обгорніть їх тегом<pre>
, для понад 10 рядків – використовуйте пісочницю (plnkr, jsbin, codepen…)