Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Різниця між script, async та defer». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**`script`, `async` та `defer`** визначають, коли браузер завантажує і запускає JavaScript відносно парсингу HTML. Звичайний `<script>` блокує парсер. `async` і `defer` завантажують паралельно, але `async` виконує одразу після завантаження, а `defer` чекає повного парсингу DOM. ```html <script src="app.js"></script> <!-- блокує парсер, виконує одразу --> <script src="track.js" async></script> <!-- паралельно, порядок не гарантовано --> <script src="app.js" defer></script> <!-- паралельно, виконує після парсингу --> ``` **Головне:** `defer` для скриптів застосунку, `async` для незалежних інструментів типу аналітики.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**`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>` | `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` для скриптів із залежностями** ```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 вже доступний.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.