Яка різниця між async та defer?
async vs defer - два атрибути HTML-тегу <script>, які обидва завантажують скрипти паралельно з парсингом HTML, але відрізняються моментом виконання: async запускає скрипт одразу після завантаження, defer чекає поки HTML-парсер завершить обробку документа.
Теорія
TL;DR
async- як замовити їжу і з'їсти її одразу як принесли, незалежно від того що відбувається за столомdefer- як спочатку доїсти основне, і лише потім їсти десерт у тому порядку, в якому замовляв- Головна різниця:
asyncможе перервати парсинг HTML для виконання скрипта;deferніколи не переривує - Правило вибору:
deferдля скриптів що працюють з DOM або залежать від інших.asyncдля незалежних сторонніх скриптів на кшталт аналітики
Швидкий приклад
<!-- Блокує парсинг: завантажує І виконує перед продовженням парсингу -->
<script src="app.js"></script>
<!-- Завантажує паралельно, виконує одразу після завантаження (може перервати парсинг) -->
<script src="analytics.js" async></script>
<!-- Завантажує паралельно, виконує після завершення парсингу, зберігає порядок -->
<script src="jquery.js" defer></script>
<script src="app.js" defer></script>Обидва атрибути починають завантаження одразу і не блокують HTML-парсер під час завантаження. Різниця проявляється в момент виконання.
Ключова різниця
async запускає скрипт одразу як він завантажився. Якщо парсинг ще триває, браузер зупиняє його, виконує скрипт, потім продовжує. Два async-скрипти можуть виконатись у будь-якому порядку залежно від того, який завантажиться швидше. defer ставить скрипт у чергу і виконує його лише після завершення парсингу HTML, у порядку появи в документі. Це робить defer передбачуваним для всього що залежить від інших скриптів або від наявності DOM.
Коли що використовувати
defer- скрипти що маніпулюють DOM, залежать від інших скриптів (React-бандли, jQuery-плагіни, ініціалізація фреймворків, аналітика що читає контент сторінки)async- сторонні скрипти що не залежать від твого коду і не потребують DOM (Google Analytics, Sentry, рекламні мережі, чат-віджети)- Жоден - inline-скрипти (
deferігнорується для них), або ES-модулі (type="module"вже відкладений за специфікацією)
Таблиця порівняння
| Аспект | Звичайний <script> | async | defer |
|---|---|---|---|
| Завантаження | Блокує парсинг | Паралельно | Паралельно |
| Виконання | Блокує парсинг | Одразу (може перервати) | Після парсингу |
| Порядок збережений | N/A | Ні | Так |
| DOM готовий при виконанні | Ні | Ні | Так |
| Найкраще для | Критичний inline-код | Незалежні сторонні скрипти | Код застосунку, залежності |
| Коли використовувати | Рідко | Аналітика, реклама, трекінг | 95% скриптів |
Як браузер це обробляє
Коли HTML-парсер зустрічає тег <script>, він вирішує що робити далі. Без атрибутів - зупиняється, завантажує, виконує, продовжує. З async - запускає паралельне завантаження і продовжує парсинг, але щойно скрипт завантажився, парсинг зупиняється і скрипт виконується. З defer - теж запускає паралельне завантаження, але виконує скрипт лише після того як парсер завершив роботу, прямо перед тим як спрацює подія DOMContentLoaded. Відкладені скрипти виконуються в порядку з документа; async-скрипти виконуються в порядку завершення завантаження, що недетерміновано.
Типова помилка команд: вважають що async завжди швидший. Для незалежних скриптів так і є. Але якщо скрипту потрібен DOM, async може виконати його до того як потрібні елементи з'явились на сторінці. Отримуємо runtime-помилки що залежать від швидкості мережі і не відтворюються стабільно.
Типові помилки
Помилка 1: async для залежних скриптів
<!-- Неправильно: jquery-plugin.js може виконатись перед jquery.js -->
<script src="jquery.js" async></script>
<script src="jquery-plugin.js" async></script>
<!-- Правильно: defer зберігає порядок -->
<script src="jquery.js" defer></script>
<script src="jquery-plugin.js" defer></script>Якщо jquery-plugin.js завантажиться швидше, він виконається першим. Отримуєш $ is not defined у рантаймі, і баг зникає при перезавантаженні на повільнішому з'єднанні. Складно відтворити, ще складніше пояснити клієнту.
Помилка 2: async і defer на одному скрипті
<!-- Неправильно: async має пріоритет, defer ігнорується -->
<script src="app.js" async defer></script>
<!-- Правильно: вибери один атрибут -->
<script src="app.js" defer></script>Браузер обробляє async defer як просто async. Такий патерн колись використовувався як запасний варіант для старих браузерів без підтримки async, але ті браузери давно зникли.
Помилка 3: defer на inline-скриптах
<!-- Неправильно: defer не впливає на inline-скрипти -->
<script defer>
console.log(document.getElementById('root')); // Все одно null
</script>
<!-- Правильно: перенеси в зовнішній файл -->
<script src="app.js" defer></script>Специфікація каже: defer працює тільки з зовнішніми скриптами через атрибут src. Inline-скрипти ігнорують цей атрибут і виконуються одразу.
Помилка 4: defer на модульних скриптах
<!-- Надлишково: type="module" вже відкладений за специфікацією -->
<script type="module" src="app.js" defer></script>
<!-- Те саме, defer не потрібен -->
<script type="module" src="app.js"></script>ES-модулі відкладені за замовчуванням. Додавати defer не потрібно.
Помилка 5: Очікувати що DOMContentLoaded спрацює всередині відкладеного скрипта
Відкладені скрипти виконуються до спрацьовування DOMContentLoaded, а не після. DOM вже готовий, але подія ще не відправлена. Якщо у відкладеному скрипті підписатись на DOMContentLoaded, цей обробник ніколи не спрацює - подія вже пройшла на момент реєстрації слухача.
Де це зустрічається
- React / Vue / Angular бандли -
defer(Next.js, Nuxt, Angular CLI використовують це за замовчуванням) - Google Analytics -
async(незалежний, не потребує DOM) - Sentry -
async(відстежує помилки незалежно) - jQuery + плагіни -
deferна обох (зберігає порядок завантаження) - Stripe / PayPal -
async(сторонній, самодостатній) - Власна аналітика що читає контент сторінки -
defer(потребує наявності DOM)
Питання на співбесіді
Q: Що станеться якщо відкладений скрипт кине помилку?
A: Цей скрипт зупинить виконання, але наступні відкладені скрипти продовжать роботу. Сторінка не зломається сама по собі, але скрипти що залежали від нього можуть не відпрацювати коректно.
Q: Яка різниця між defer і скриптами в кінці </body>?
A: Функціонально схоже за порядком виконання. Але defer у <head> починає завантаження раніше, під час парсингу HTML, тому скрипт готовий швидше. Скрипти в кінці body - це старий підхід до появи нормальної підтримки defer. Сьогодні правильно: defer у <head>.
Q: Чи можна використовувати async з type="module"?
A: Можна. Але модулі вже відкладені за замовчуванням, тому async змусить їх виконуватись одразу після завантаження як класичні async-скрипти. Це руйнує порядок залежностей (dependency order) який модулі зазвичай зберігають. Рідко буває корисним.
Q: Є три скрипти: A залежить від B, B незалежний, C залежить від A. Як оптимально їх завантажити?
A: B завантажуй з async - немає залежностей. A і C завантажуй з defer - гарантує що A виконається перед C. B завантажується паралельно і виконується коли готовий; A і C виконуються в порядку документа після парсингу. Якщо B критичний для A, використовуй defer для всіх трьох.
Приклади
Базовий: async і доступ до DOM
<head>
<script src="analytics.js" async></script>
</head>
<body>
<div id="app">Loading...</div>
</body>// analytics.js
// Може виконатись до того як <div id="app"> буде розпарсений
const app = document.getElementById('app');
console.log(app); // null якщо HTML ще не розпарсенийanalytics.js починає завантажуватись одразу як браузер бачить тег <script>. На швидкому з'єднанні він може завантажитись до того як тіло документа розпарсено, і getElementById повертає null. З defer це читання безпечне, бо виконання чекає завершення парсингу.
Середній: ініціалізація React з defer
<head>
<script src="react.js" defer></script>
<script src="react-dom.js" defer></script>
<script src="app.js" defer></script>
</head>
<body>
<div id="root"></div>
</body>// app.js - виконується після парсингу HTML, після react.js і react-dom.js
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);Всі три скрипти завантажуються паралельно під час парсингу HTML. Виконуються по порядку: react.js перший, потім react-dom.js, потім app.js. На момент запуску app.js - ReactDOM доступний і #root існує в DOM. Саме так Next.js генерує теги скриптів для кожної сторінки за замовчуванням.
Просунутий: race condition з async
// Баг: два async-скрипти із залежністю
// <script src="jquery.js" async></script>
// <script src="jquery-plugin.js" async></script>
// Таймлайн на швидкому CDN-з'єднанні:
// t=0ms: обидва скрипти починають завантаження
// t=40ms: jquery-plugin.js прийшов (менший файл) -> виконується -> ПОМИЛКА: $ is not defined
// t=80ms: jquery.js прийшов -> виконується, але плагін вже впав
// Таймлайн на повільному з'єднанні:
// t=0ms: обидва скрипти починають завантаження
// t=200ms: jquery.js приходить першим через перезамовлення мережі -> виконується
// t=250ms: jquery-plugin.js приходить -> $ існує -> працює чудово
// Баг з'являється тільки на швидких з'єднаннях. Проходить локальні тести, падає на CDN.
// Виправлення: defer на обох для гарантованого порядку
// <script src="jquery.js" defer></script>
// <script src="jquery-plugin.js" defer></script>
// Аналітика може залишитись async бо немає залежностей
// <script src="google-analytics.js" async></script>Такий баг проходить усі тести локально і падає в продакшені під навантаженням. Порядок виконання async-скриптів залежить від того які байти прийдуть першими, а це змінюється залежно від мережевих умов, CDN-вузла і розміру файлів. defer прибирає цю змінну повністю.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.