Skip to main content

Різниця між script, async та defer

script, async та defer визначають, коли браузер завантажує і запускає JavaScript відносно парсингу HTML.

Теорія

TL;DR

  • Звичайний <script> зупиняє парсер HTML, завантажує файл, виконує його і тільки потім продовжує. Блокуючий.
  • async завантажує файл паралельно і запускає його одразу після завантаження. Порядок не гарантовано.
  • defer також завантажує паралельно, але чекає поки весь HTML буде спарсений. Потім виконує скрипти в порядку оголошення.
  • Аналогія: парсер HTML це кухар, що читає рецепт. Звичайний <script> змушує кухаря зупинити все і одразу виконати завдання. async передає задачу помічнику, але той може перервати в будь-який момент. defer теж передає помічнику, але той включається тільки коли кухар дочитає весь рецепт і строго по черзі.
  • Правило вибору: defer для скриптів застосунку, async для незалежних інструментів типу аналітики.

Швидкий приклад

html
<!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>asyncdefer
Блокує HTML парсингТак (завантаження + виконання)Ні (завантаження), коротко (виконання)Ні
ЗавантаженняСинхроннеПаралельнеПаралельне
Момент виконанняОдразу при зустрічіКоли завантаження завершеноПісля повного парсингу HTML
Гарантія порядкуН/ДНіТак
Доступ до DOMТільки вище тегуНенадійний (може бути середина парсингу)Повний
Для чого підходитьМаленький інлайн, кінець bodyНезалежні скрипти (аналітика, реклама)Скрипти застосунку, бібліотеки

Як браузер це обробляє

Коли парсер натрапляє на звичайний <script>, він зупиняється, передає URL мережевому рівню, чекає, потім виконує скрипт у головному потоці. async і defer підхоплюються preload scanner'ом - окремим спекулятивним прогоном, що читає HTML вперед основного парсера без побудови DOM. Різниця в черзі. async скрипти виконуються одразу після завантаження. defer скрипти потрапляють в упорядкований список і запускаються після того, як document.readyState стає "interactive", до події DOMContentLoaded.

Це часто плутають на співбесідах: defer не має ефекту на інлайн-скрипти без атрибута src. Це прописано в HTML-специфікації. Інлайн-код із defer виконується одразу, як звичайний <script>.

Типові помилки

async для скриптів із залежностями

html
<!-- Неправильно: 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 на інлайн-скрипті

html
<!-- Неправильно: defer ігнорується, скрипт виконується одразу --> <script defer> document.getElementById('root').innerHTML = 'Hello'; </script> <!-- Правильно: загорнути в DOMContentLoaded --> <script> document.addEventListener('DOMContentLoaded', () => { document.getElementById('root').innerHTML = 'Hello'; }); </script>

async і слухач DOMContentLoaded всередині скрипту

html
<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-скрипт.

Приклади

Блокуючий скрипт затримує відображення сторінки

html
<!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

html
<!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

html
<!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 вже доступний.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?