Представь: твой лендинг грузится 4 секунды — пользователь видит белый экран. Причина: скрипт аналитики в <head> без атрибутов блокирует весь HTML. Два атрибута — async и defer — полностью меняют поведение загрузки скриптов и напрямую влияют на Core Web Vitals и конверсию.
По умолчанию <script> в <head> останавливает парсинг HTML до полной загрузки и выполнения скрипта. На медленном 3G это может добавить 2-3 секунды к First Contentful Paint. defer и async позволяют загружать скрипты параллельно с парсингом HTML.
defer влияет на момент DOMContentLoaded, async — нетPromise.all<!-- Браузер СТОПОРИТ парсинг HTML, скачивает, выполняет, потом продолжает -->
<head>
<script src="heavy-library.js"></script> <!-- блокирует! -->
</head>
<body>
<!-- Эта часть не начнёт рендериться до завершения скрипта -->
</body>Диаграмма обычного скрипта:
HTML: ████████ СТОП ████████
скрипт: ░░░░ ████
^load ^exec<script defer src="app.js"></script>
<script defer src="utils.js"></script>DOMContentLoaded ждёт выполнения всех defer-скриптовДиаграмма defer:
HTML: ████████████████████ DOMContentLoaded
скрипт: ░░░░░░░░░░░ ████
^начало загрузки ^выполнение после HTML<script async src="analytics.js"></script>
<script async src="ads.js"></script>DOMContentLoaded НЕ ждёт async-скриптовДиаграмма async:
HTML: ████████ СТОП ████████████████
скрипт: ░░░░░ ████
^загрузился — СРАЗУ выполнился| | Обычный | defer | async |
|---|---|---|---|
| Блокирует HTML | Да | Нет | Частично |
| Параллельная загрузка | Нет | Да | Да |
| Порядок выполнения | Порядок в HTML | Порядок в HTML | Первый загрузился — первый выполнился |
| Ждёт DOM | Не нужно | Да | Нет |
| DOMContentLoaded | До него | После него | Независимо |
<!-- Сторонняя аналитика, метрики, рекламные скрипты -->
<!-- Не зависят от DOM, не зависят друг от друга -->
<script async src="https://analytics.example.com/tracker.js"></script>
<!-- Основной код приложения, библиотеки -->
<!-- Зависят от DOM, порядок важен -->
<script defer src="vendor/jquery.js"></script>
<script defer src="app.js"></script> <!-- выполнится ПОСЛЕ jquery.js -->
<!-- Критичный инлайновый код -->
<script>
// Инлайн-скрипты игнорируют async/defer
const config = { theme: 'dark' }
</script>// DOMContentLoaded — DOM готов, картинки ещё грузятся
// Срабатывает ПОСЛЕ defer-скриптов
document.addEventListener('DOMContentLoaded', () => {
console.log('DOM готов, можно работать с элементами')
})
// load — всё готово: DOM, картинки, стили, шрифты
window.addEventListener('load', () => {
console.log('Всё загружено включая картинки')
})1. Использовать async для скриптов, которые зависят друг от друга
<!-- ПЛОХО — async не гарантирует порядок! -->
<script async src="jquery.js"></script>
<script async src="app.js"></script>
<!-- app.js может выполниться ДО jquery.js — TypeError: $ is not defined -->
<!-- ХОРОШО — defer сохраняет порядок -->
<script defer src="jquery.js"></script>
<script defer src="app.js"></script>
<!-- jquery.js всегда выполнится первым -->2. Добавлять async/defer к инлайновым скриптам — атрибуты игнорируются
<!-- ПЛОХО — async/defer не работают для инлайн-скриптов -->
<script async>
const config = { theme: 'dark' } // выполняется синхронно несмотря на async
</script>
<!-- async/defer работают ТОЛЬКО для внешних файлов с src -->
<script defer src="app.js"></script>3. Размещать defer-скрипты в body перед закрывающим тегом — это старый паттерн
<!-- УСТАРЕЛО — скрипты в конце body были хаком до появления defer -->
<body>
...контент...
<script src="app.js"></script> <!-- блокирует только конец страницы -->
</body>
<!-- СОВРЕМЕННЫЙ ПОДХОД — defer в head, HTML парсится полностью параллельно -->
<head>
<script defer src="app.js"></script>
</head>async — независимы от DOM, не должны блокировать контентmain.js подключается через defer — DOM нужен, порядок важенdefer/async на тяжёлых скриптах — одна из частых причин плохого LCP и FID в LighthouseСимуляция порядка загрузки async vs defer через Promise и setTimeout — наглядная демонстрация разницы
// Симуляция порядка загрузки скриптов через Promise + setTimeout
// Числа в setTimeout имитируют время загрузки скриптов с сети
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
// --- Симуляция обычного (блокирующего) скрипта ---
async function simulateNormalScript() {
const log = []
log.push('Начало парсинга HTML...')
// Блокирует парсинг
log.push('[СТОП] Загружаем script.js (300мс)...')
await delay(300)
log.push('[ВЫПОЛНЕНИЕ] script.js запущен')
log.push('Продолжаем парсинг HTML...')
log.push('HTML полностью разобран')
log.push('DOMContentLoaded!')
return log
}
// --- Симуляция defer ---
async function simulateDeferScripts() {
const log = []
log.push('Начало парсинга HTML...')
// Параллельная загрузка
const deferLoads = [
delay(200).then(() => ({ name: 'vendor.js', order: 1 })),
delay(300).then(() => ({ name: 'app.js', order: 2 })),
delay(100).then(() => ({ name: 'utils.js', order: 0 })),
]
// HTML парсится одновременно
await delay(250)
log.push('HTML полностью разобран (defer скрипты ещё грузятся)')
// Ждём ВСЕ defer-скрипты, выполняем В ПОРЯДКЕ order
const loaded = await Promise.all(deferLoads)
const sorted = loaded.sort((a, b) => a.order - b.order)
sorted.forEach(s => log.push(`[ВЫПОЛНЕНИЕ defer] ${s.name}`))
log.push('DOMContentLoaded! (после всех defer)')
return log
}
// --- Симуляция async ---
async function simulateAsyncScripts() {
const log = []
log.push('Начало парсинга HTML...')
// Каждый загружается и выполняется как только готов (без порядка)
const asyncScripts = [
delay(250).then(() => 'analytics.js'), // загружается 250мс
delay(100).then(() => 'tracker.js'), // загружается 100мс (первым!)
delay(400).then(() => 'ads.js'), // загружается 400мс (последним)
]
// HTML парсится параллельно
const htmlParsing = delay(350).then(() => {
log.push('HTML полностью разобран')
log.push('DOMContentLoaded! (async скрипты ещё могут выполняться)')
})
// Выполняем по мере загрузки (порядок НЕ гарантирован)
asyncScripts.forEach(p => {
p.then(name => log.push(`[ВЫПОЛНЕНИЕ async] ${name} — загрузился!`))
})
await Promise.all([...asyncScripts, htmlParsing])
return log
}
// --- Запуск всех симуляций ---
console.log('=== Обычный скрипт (блокирующий) ===')
simulateNormalScript().then(log => {
log.forEach(line => console.log(line))
console.log('\n=== defer скрипты ===')
return simulateDeferScripts()
}).then(log => {
log.forEach(line => console.log(line))
console.log('\n=== async скрипты ===')
return simulateAsyncScripts()
}).then(log => {
log.forEach(line => console.log(line))
console.log('\n=== Итог: порядок выполнения ===')
console.log('Обычный: HTML СТОП → скрипт → HTML продолжение')
console.log('defer: HTML + загрузка параллельно → выполнение в порядке → DOMContentLoaded')
console.log('async: HTML + загрузка параллельно → выполнение по готовности (порядок случайный)')
})Представь: твой лендинг грузится 4 секунды — пользователь видит белый экран. Причина: скрипт аналитики в <head> без атрибутов блокирует весь HTML. Два атрибута — async и defer — полностью меняют поведение загрузки скриптов и напрямую влияют на Core Web Vitals и конверсию.
По умолчанию <script> в <head> останавливает парсинг HTML до полной загрузки и выполнения скрипта. На медленном 3G это может добавить 2-3 секунды к First Contentful Paint. defer и async позволяют загружать скрипты параллельно с парсингом HTML.
defer влияет на момент DOMContentLoaded, async — нетPromise.all<!-- Браузер СТОПОРИТ парсинг HTML, скачивает, выполняет, потом продолжает -->
<head>
<script src="heavy-library.js"></script> <!-- блокирует! -->
</head>
<body>
<!-- Эта часть не начнёт рендериться до завершения скрипта -->
</body>Диаграмма обычного скрипта:
HTML: ████████ СТОП ████████
скрипт: ░░░░ ████
^load ^exec<script defer src="app.js"></script>
<script defer src="utils.js"></script>DOMContentLoaded ждёт выполнения всех defer-скриптовДиаграмма defer:
HTML: ████████████████████ DOMContentLoaded
скрипт: ░░░░░░░░░░░ ████
^начало загрузки ^выполнение после HTML<script async src="analytics.js"></script>
<script async src="ads.js"></script>DOMContentLoaded НЕ ждёт async-скриптовДиаграмма async:
HTML: ████████ СТОП ████████████████
скрипт: ░░░░░ ████
^загрузился — СРАЗУ выполнился| | Обычный | defer | async |
|---|---|---|---|
| Блокирует HTML | Да | Нет | Частично |
| Параллельная загрузка | Нет | Да | Да |
| Порядок выполнения | Порядок в HTML | Порядок в HTML | Первый загрузился — первый выполнился |
| Ждёт DOM | Не нужно | Да | Нет |
| DOMContentLoaded | До него | После него | Независимо |
<!-- Сторонняя аналитика, метрики, рекламные скрипты -->
<!-- Не зависят от DOM, не зависят друг от друга -->
<script async src="https://analytics.example.com/tracker.js"></script>
<!-- Основной код приложения, библиотеки -->
<!-- Зависят от DOM, порядок важен -->
<script defer src="vendor/jquery.js"></script>
<script defer src="app.js"></script> <!-- выполнится ПОСЛЕ jquery.js -->
<!-- Критичный инлайновый код -->
<script>
// Инлайн-скрипты игнорируют async/defer
const config = { theme: 'dark' }
</script>// DOMContentLoaded — DOM готов, картинки ещё грузятся
// Срабатывает ПОСЛЕ defer-скриптов
document.addEventListener('DOMContentLoaded', () => {
console.log('DOM готов, можно работать с элементами')
})
// load — всё готово: DOM, картинки, стили, шрифты
window.addEventListener('load', () => {
console.log('Всё загружено включая картинки')
})1. Использовать async для скриптов, которые зависят друг от друга
<!-- ПЛОХО — async не гарантирует порядок! -->
<script async src="jquery.js"></script>
<script async src="app.js"></script>
<!-- app.js может выполниться ДО jquery.js — TypeError: $ is not defined -->
<!-- ХОРОШО — defer сохраняет порядок -->
<script defer src="jquery.js"></script>
<script defer src="app.js"></script>
<!-- jquery.js всегда выполнится первым -->2. Добавлять async/defer к инлайновым скриптам — атрибуты игнорируются
<!-- ПЛОХО — async/defer не работают для инлайн-скриптов -->
<script async>
const config = { theme: 'dark' } // выполняется синхронно несмотря на async
</script>
<!-- async/defer работают ТОЛЬКО для внешних файлов с src -->
<script defer src="app.js"></script>3. Размещать defer-скрипты в body перед закрывающим тегом — это старый паттерн
<!-- УСТАРЕЛО — скрипты в конце body были хаком до появления defer -->
<body>
...контент...
<script src="app.js"></script> <!-- блокирует только конец страницы -->
</body>
<!-- СОВРЕМЕННЫЙ ПОДХОД — defer в head, HTML парсится полностью параллельно -->
<head>
<script defer src="app.js"></script>
</head>async — независимы от DOM, не должны блокировать контентmain.js подключается через defer — DOM нужен, порядок важенdefer/async на тяжёлых скриптах — одна из частых причин плохого LCP и FID в LighthouseСимуляция порядка загрузки async vs defer через Promise и setTimeout — наглядная демонстрация разницы
// Симуляция порядка загрузки скриптов через Promise + setTimeout
// Числа в setTimeout имитируют время загрузки скриптов с сети
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
// --- Симуляция обычного (блокирующего) скрипта ---
async function simulateNormalScript() {
const log = []
log.push('Начало парсинга HTML...')
// Блокирует парсинг
log.push('[СТОП] Загружаем script.js (300мс)...')
await delay(300)
log.push('[ВЫПОЛНЕНИЕ] script.js запущен')
log.push('Продолжаем парсинг HTML...')
log.push('HTML полностью разобран')
log.push('DOMContentLoaded!')
return log
}
// --- Симуляция defer ---
async function simulateDeferScripts() {
const log = []
log.push('Начало парсинга HTML...')
// Параллельная загрузка
const deferLoads = [
delay(200).then(() => ({ name: 'vendor.js', order: 1 })),
delay(300).then(() => ({ name: 'app.js', order: 2 })),
delay(100).then(() => ({ name: 'utils.js', order: 0 })),
]
// HTML парсится одновременно
await delay(250)
log.push('HTML полностью разобран (defer скрипты ещё грузятся)')
// Ждём ВСЕ defer-скрипты, выполняем В ПОРЯДКЕ order
const loaded = await Promise.all(deferLoads)
const sorted = loaded.sort((a, b) => a.order - b.order)
sorted.forEach(s => log.push(`[ВЫПОЛНЕНИЕ defer] ${s.name}`))
log.push('DOMContentLoaded! (после всех defer)')
return log
}
// --- Симуляция async ---
async function simulateAsyncScripts() {
const log = []
log.push('Начало парсинга HTML...')
// Каждый загружается и выполняется как только готов (без порядка)
const asyncScripts = [
delay(250).then(() => 'analytics.js'), // загружается 250мс
delay(100).then(() => 'tracker.js'), // загружается 100мс (первым!)
delay(400).then(() => 'ads.js'), // загружается 400мс (последним)
]
// HTML парсится параллельно
const htmlParsing = delay(350).then(() => {
log.push('HTML полностью разобран')
log.push('DOMContentLoaded! (async скрипты ещё могут выполняться)')
})
// Выполняем по мере загрузки (порядок НЕ гарантирован)
asyncScripts.forEach(p => {
p.then(name => log.push(`[ВЫПОЛНЕНИЕ async] ${name} — загрузился!`))
})
await Promise.all([...asyncScripts, htmlParsing])
return log
}
// --- Запуск всех симуляций ---
console.log('=== Обычный скрипт (блокирующий) ===')
simulateNormalScript().then(log => {
log.forEach(line => console.log(line))
console.log('\n=== defer скрипты ===')
return simulateDeferScripts()
}).then(log => {
log.forEach(line => console.log(line))
console.log('\n=== async скрипты ===')
return simulateAsyncScripts()
}).then(log => {
log.forEach(line => console.log(line))
console.log('\n=== Итог: порядок выполнения ===')
console.log('Обычный: HTML СТОП → скрипт → HTML продолжение')
console.log('defer: HTML + загрузка параллельно → выполнение в порядке → DOMContentLoaded')
console.log('async: HTML + загрузка параллельно → выполнение по готовности (порядок случайный)')
})Напиши функцию simulateLoadOrder(scripts) которая принимает массив скриптов вида [{ name, loadTime, type }] где type — "normal", "defer" или "async". Функция должна вернуть Promise, который резолвится в массив строк — порядок событий (загрузка, выполнение, DOMContentLoaded). Для normal: каждый блокирует HTML. Для defer: все грузятся параллельно, выполняются в исходном порядке после HTML. Для async: каждый выполняется как только загрузился.
delay(s.loadTime) для загрузки. deferLoads — Promise.all, потом sort по idx. asyncLoads — каждый выполняется сразу в .then(). HTML_PARSE_TIME = 200мс симулирует парсинг.