Різниця між script, async та defer
script, async та defer визначають, коли браузер завантажує і запускає JavaScript відносно парсингу HTML.
Теорія
TL;DR
- Звичайний
<script>зупиняє парсер HTML, завантажує файл, виконує його і тільки потім продовжує. Блокуючий. asyncзавантажує файл паралельно і запускає його одразу після завантаження. Порядок не гарантовано.deferтакож завантажує паралельно, але чекає поки весь HTML буде спарсений. Потім виконує скрипти в порядку оголошення.- Аналогія: парсер HTML це кухар, що читає рецепт. Звичайний
<script>змушує кухаря зупинити все і одразу виконати завдання.asyncпередає задачу помічнику, але той може перервати в будь-який момент.deferтеж передає помічнику, але той включається тільки коли кухар дочитає весь рецепт і строго по черзі. - Правило вибору:
deferдля скриптів застосунку,asyncдля незалежних інструментів типу аналітики.
Швидкий приклад
<!DOCTYPE html>
<html>
<head>
<!-- Блокує парсер до завантаження і виконання -->
<script src="slow.js"></script>
<!-- Завантажує паралельно, виконує коли готовий (порядок не гарантовано) -->
<script src="analytics.js" async></script>
<!-- Завантажує паралельно, виконує після повного парсингу, по порядку -->
<script src="app.js" defer></script>
</head>
<body>
<div id="root"></div>
</body>
</html>Якщо slow.js завантажується 2 секунди, async і defer не затримують парсер. Звичайний скрипт затримує.
Головна різниця
Ключовий момент це коли відбувається виконання. І async, і defer використовують preload scanner браузера для завантаження файлів без блокування парсера. Але async запускає скрипт одразу після завершення завантаження, навіть якщо HTML ще не дочитаний. DOM у цей момент неповний. defer поміщає скрипт в упорядковану чергу і виконує її тільки після того, як парсер завершить роботу, але до події DOMContentLoaded.
Коли що використовувати
- Основний бандл застосунку (React, Vue, vanilla JS) -
defer. DOM готовий, порядок збережено. - Аналітика та реклама (Google Analytics, Facebook Pixel) -
async. Не залежать від елементів сторінки. - Кілька скриптів із залежностями (jQuery + плагін) -
deferна обох. Порядок оголошення зберігається. - Маленький критичний інлайн-код - звичайний
<script>в<head>. Завантаження не потрібне, виконується миттєво. - Звичайний скрипт в кінці body - прийнятно для невеликих сторінок, але
deferв<head>чистіше.
Таблиця порівняння
| Аспект | <script> | async | defer |
|---|---|---|---|
| Блокує HTML парсинг | Так (завантаження + виконання) | Ні (завантаження), коротко (виконання) | Ні |
| Завантаження | Синхронне | Паралельне | Паралельне |
| Момент виконання | Одразу при зустрічі | Коли завантаження завершено | Після повного парсингу HTML |
| Гарантія порядку | Н/Д | Ні | Так |
| Доступ до DOM | Тільки вище тегу | Ненадійний (може бути середина парсингу) | Повний |
| Для чого підходить | Маленький інлайн, кінець body | Незалежні скрипти (аналітика, реклама) | Скрипти застосунку, бібліотеки |
Як браузер це обробляє
Коли парсер натрапляє на звичайний <script>, він зупиняється, передає URL мережевому рівню, чекає, потім виконує скрипт у головному потоці. async і defer підхоплюються preload scanner'ом - окремим спекулятивним прогоном, що читає HTML вперед основного парсера без побудови DOM. Різниця в черзі. async скрипти виконуються одразу після завантаження. defer скрипти потрапляють в упорядкований список і запускаються після того, як document.readyState стає "interactive", до події DOMContentLoaded.
Це часто плутають на співбесідах: defer не має ефекту на інлайн-скрипти без атрибута src. Це прописано в HTML-специфікації. Інлайн-код із defer виконується одразу, як звичайний <script>.
Типові помилки
async для скриптів із залежностями
<!-- Неправильно: initMap() може запуститись до завантаження maps.js -->
<script async src="maps.js"></script>
<script async src="app.js"></script> <!-- app.js викликає initMap() -->
<!-- Правильно: defer зберігає порядок -->
<script defer src="maps.js"></script>
<script defer src="app.js"></script>async перетворює кожен скрипт на перегони. Якщо app.js завантажиться першим, initMap ще не визначено. Помилки в консолі може і не бути, просто нічого не спрацює.
defer на інлайн-скрипті
<!-- Неправильно: defer ігнорується, скрипт виконується одразу -->
<script defer>
document.getElementById('root').innerHTML = 'Hello';
</script>
<!-- Правильно: загорнути в DOMContentLoaded -->
<script>
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('root').innerHTML = 'Hello';
});
</script>async і слухач DOMContentLoaded всередині скрипту
<script async src="app.js"></script>
<!-- app.js: document.addEventListener('DOMContentLoaded', init) -->Якщо app.js завантажується після того, як DOMContentLoaded вже спрацював, init не виконається ніколи. Жодної помилки. defer вирішує це: деферовані скрипти запускаються до DOMContentLoaded, тому слухач завжди встигає зареєструватися.
Блокуючий <script> в <head> для важливого коду
Скрипт, що блокує рендер у <head>, затримує First Contentful Paint. Навіть 100ms мережевий запит помітно впливає на метрики Lighthouse. Варто переносити в кінець body або додавати defer.
Де зустрічається в реальних проектах
- React (Vite / CRA) -
deferна бандлі; DOM готовий коли запускаєтьсяReactDOM.createRoot. - Google Analytics (gtag.js) -
asyncв<head>; трекінг-піксель без залежностей від DOM. - jQuery + плагіни Bootstrap -
deferна обох; jQuery завантажується першим, плагін знаходить його. - Facebook Pixel -
async; надсилає події незалежно від стану сторінки. - Webpack з
html-webpack-plugin- підставляєdeferна вихідні чанки за замовчуванням, тому більшість React-застосунків просто працюють без додаткових налаштувань.
Питання на співбесіді
Q: Що станеться якщо вказати одночасно async і defer?
A: Перемагає async. Браузери ігнорують defer, якщо обидва атрибути присутні.
Q: Чи гарантує defer виконання до DOMContentLoaded?
A: Так. Деферовані скрипти виконуються після повного парсингу HTML і до спрацювання DOMContentLoaded. Це контракт, прописаний у специфікації.
Q: Чи можна використати defer на інлайн-скрипті?
A: Ні. defer не має ефекту на скрипти без src. Вони виконуються одразу незалежно від атрибута.
Q: Як preload scanner виявляє async і defer до того, як парсер дійшов до тегу?
A: Preload scanner - це окремий токенізатор, що читає HTML-потік без побудови DOM. Він знаходить атрибути src у <script async> і <script defer> заздалегідь і ставить мережеві запити в чергу. До того як основний парсер дійде до тегу, файл вже завантажується.
Q: Як поводяться скрипти з type="module" порівняно з defer?
A: Module-скрипти за замовчуванням поводяться як defer. Завантажуються паралельно, виконуються після парсингу, по порядку. Додавання async до модуля робить його поведінку схожою на звичайний async-скрипт.
Приклади
Блокуючий скрипт затримує відображення сторінки
<!DOCTYPE html>
<html>
<head>
<title>Блокуючий приклад</title>
<!-- Завантажується з мережі перед рендером будь-якого контенту body -->
<script src="heavy-analytics.js"></script>
</head>
<body>
<h1>Користувачі бачать це пізніше ніж могли б</h1>
</body>
</html><h1> не відмальовується поки heavy-analytics.js не завантажиться і не виконається. Переміщення в async або в кінець body розблоковує рендер.
React-застосунок із defer
<!DOCTYPE html>
<html>
<head>
<title>App</title>
<!-- Три скрипти завантажуються паралельно, але виконуються по порядку -->
<script defer src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script defer src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script defer src="bundle.js"></script>
</head>
<body>
<div id="root"></div>
<!-- До моменту виконання bundle.js елемент #root вже існує в DOM -->
</body>
</html>defer зберігає порядок: React, ReactDOM, потім бандл. #root вже є в DOM коли викликається ReactDOM.createRoot. Ніяких setTimeout або перевірок readyState.
Гонка станів із async
<!DOCTYPE html>
<html>
<head>
<!-- async запускає скрипт коли готовий, потенційно до парсингу body -->
<script async src="tracker.js"></script>
</head>
<body>
<form id="signup">...</form> <!-- Може не існувати коли виконається tracker.js -->
</body>
</html>Якщо tracker.js викликає document.getElementById('signup'), результат може бути null. Форму ще не спарсено. defer повністю усуває цю проблему: на момент виконання повний DOM вже доступний.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.