Skip to main content

Яка різниця між async та defer?

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>asyncdefer
ЗавантаженняБлокує парсингПаралельноПаралельно
ВиконанняБлокує парсингОдразу (може перервати)Після парсингу
Порядок збережений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 прибирає цю змінну повністю.

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

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

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

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