Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Яка різниця між async та defer?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**async vs defer** - обидва завантажують скрипти паралельно з парсингом HTML, але виконуються в різний момент. ```html <script src="analytics.js" async></script> <!-- виконується одразу, порядок не гарантований --> <script src="app.js" defer></script> <!-- виконується після парсингу, по порядку --> ``` **Ключове:** `async` для незалежних сторонніх скриптів; `defer` для коду застосунку і всього що має залежності.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**async vs defer** - два атрибути HTML-тегу `<script>`, які обидва завантажують скрипти паралельно з парсингом HTML, але відрізняються моментом виконання: `async` запускає скрипт одразу після завантаження, `defer` чекає поки HTML-парсер завершить обробку документа. ## Теорія ### TL;DR - `async` - як замовити їжу і з'їсти її одразу як принесли, незалежно від того що відбувається за столом - `defer` - як спочатку доїсти основне, і лише потім їсти десерт у тому порядку, в якому замовляв - Головна різниця: `async` може перервати парсинг HTML для виконання скрипта; `defer` ніколи не переривує - Правило вибору: `defer` для скриптів що працюють з DOM або залежать від інших. `async` для незалежних сторонніх скриптів на кшталт аналітики ### Швидкий приклад ```html <!-- Блокує парсинг: завантажує І виконує перед продовженням парсингу --> <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` для залежних скриптів** ```html <!-- Неправильно: 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` на одному скрипті** ```html <!-- Неправильно: async має пріоритет, defer ігнорується --> <script src="app.js" async defer></script> <!-- Правильно: вибери один атрибут --> <script src="app.js" defer></script> ``` Браузер обробляє `async defer` як просто `async`. Такий патерн колись використовувався як запасний варіант для старих браузерів без підтримки `async`, але ті браузери давно зникли. **Помилка 3: `defer` на inline-скриптах** ```html <!-- Неправильно: defer не впливає на inline-скрипти --> <script defer> console.log(document.getElementById('root')); // Все одно null </script> <!-- Правильно: перенеси в зовнішній файл --> <script src="app.js" defer></script> ``` Специфікація каже: `defer` працює тільки з зовнішніми скриптами через атрибут `src`. Inline-скрипти ігнорують цей атрибут і виконуються одразу. **Помилка 4: `defer` на модульних скриптах** ```html <!-- Надлишково: 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 ```html <head> <script src="analytics.js" async></script> </head> <body> <div id="app">Loading...</div> </body> ``` ```javascript // analytics.js // Може виконатись до того як <div id="app"> буде розпарсений const app = document.getElementById('app'); console.log(app); // null якщо HTML ще не розпарсений ``` `analytics.js` починає завантажуватись одразу як браузер бачить тег `<script>`. На швидкому з'єднанні він може завантажитись до того як тіло документа розпарсено, і `getElementById` повертає `null`. З `defer` це читання безпечне, бо виконання чекає завершення парсингу. ### Середній: ініціалізація React з defer ```html <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> ``` ```javascript // 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 ```javascript // Баг: два 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` прибирає цю змінну повністю.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.