17 лютого 2022 р.

Скрипти: async, defer

На сучасних вебсайтах скрипти часто “важчі” за HTML: їх розмір завантаження більший, і час обробки теж довший.

Коли браузер завантажує HTML і зустрічає тег <script>...</script>, він не може продовжувати будівництво DOM. Він повинен виконати скрипт прямо зараз. Те ж саме відбувається для зовнішніх скриптів <script src="..."></script>: браузер повинен зачекати, щоб скрипт завантажився, виконати завантажений скрипт, і тільки тоді він може обробити решту сторінки.

Це призводить до двох важливих проблем:

  1. Скрипти не можуть бачити елементи DOM під ними, тому вони не можуть додавати обробники тощо.
  2. Якщо у верхній частині сторінки є громіздкий скрипт, він “блокує сторінку”. Користувачі не можуть побачити вміст сторінки, поки він не завантажиться та не запуститься:
<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>
  1. Вміст сторінки з’являється негайно.
  2. Обробник події 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, користувач побачить сторінку перед завантаженням скрипту.

У такому випадку деякі графічні компоненти, ймовірно, ще не ініціалізовано.

Не забудьте поставити індикацію “завантаження” та відключити кнопки, які ще не працюють. Нехай користувач чітко бачить, що він може робити на сторінці, а що ще готується.

Навчальна карта