Скрипти в head
Скрипт у <head> без атрибутів defer чи async зупиняє парсинг HTML: браузер мусить завантажити й виконати JS, перш ніж продовжити будувати DOM. Через це користувач бачить порожню сторінку доти, доки файл не прийде по мережі, і всі метрики швидкості погіршуються.
Теорія
TL;DR
- Синхронний
<script src>блокує HTML-парсер, доки файл не завантажиться і не виконається. - Браузер робить так, бо скрипт може змінити документ, наприклад через
document.write. - У
<head>це найболючіше: DOM ще порожній, тому екран білий. - Псуються FCP, LCP, TTI та TBT, особливо на мобільній мережі.
- Ліки:
deferдля основного коду,asyncдля незалежних скриптів, або тег у кінці<body>. - Маленький inline-скрипт у
<head>допустимий, якщо він справді маленький.
Швидкий приклад
<head>
<script src="main.js"></script>
</head>
<body>
<h1>Привіт!</h1>
</body>Покроково:
- Браузер починає читати HTML.
- Доходить до
<script src="main.js">. - Зупиняє парсинг HTML.
- Чекає, поки
main.jsзавантажиться по мережі, а це можуть бути сотні мілісекунд. - Виконує скрипт.
- І лише потім продовжує парсити решту сторінки.
Увесь цей час DOM ще не створено, користувач не бачить контенту, а метрики FCP і LCP страждають.
Як браузер завантажує сторінку
Коли браузер отримує HTML-документ, він виконує такі кроки:
- Парсить HTML рядок за рядком, згори вниз.
- Коли зустрічає тег
<link>з CSS, завантажує стиль, бо стилі впливають на відображення. - Коли зустрічає тег
<script>, зупиняє парсинг, щоб виконати JS.
Чому так? Тому що скрипт може змінити сам DOM або навіть видалити елементи, які йдуть далі в HTML (
document.write,innerHTMLтощо). Тому браузер зобов'язаний дочекатися виконання JS, перш ніж продовжити розбір.
Схематично синхронний варіант виглядає так:
HTML: <head> -> <script> -> <body>
| завантаження
[======= JS завантажується =======]
| виконання
[======= JS виконується =======]
| і лише потім будується DOMА з defer завантаження йде паралельно, а виконання відкладається на кінець:
HTML і JS завантажуються разом -> DOM побудовано -> JS виконуєтьсяЯкі проблеми це спричиняє
| Проблема | Що відбувається |
|---|---|
| Блокується HTML-парсер | Сторінка не будується, доки не завантажиться JS |
| Довга «біла сторінка» | Користувач бачить порожнечу |
| Повільні мережі, гірший UX | Особливо на 3G і 4G |
| Погіршення Core Web Vitals | LCP, FCP, TTI, TBT зростають |
| Скрипти заважають критичному CSS | Критичні стилі застосовуються невчасно |
Як уникнути блокування
Спосіб 1. defer
<script src="main.js" defer></script>Скрипт завантажується паралельно з HTML, але виконується лише після повного парсингу DOM, перед подією DOMContentLoaded. Без блокування, з гарантованим порядком виконання, і це ідеальний варіант для більшості випадків.
Спосіб 2. async
<script src="analytics.js" async></script>Скрипт завантажується асинхронно, у фоні, і виконується одразу після завантаження, не чекаючи інших. Використовується для незалежних скриптів: аналітики, реклами, пікселів. Порядок виконання не гарантується.
Спосіб 3. Перенести скрипти в кінець <body>
<body>
...
<script src="main.js"></script>
</body>До моменту завантаження JS HTML вже повністю розпарсено, користувач бачить контент, а JS виконується останнім кроком. Це старий, але робочий спосіб, у сучасному HTML його замінив <script defer>.
Вплив на метрики
| Метрика | Що погіршується при скриптах у <head> |
|---|---|
| FCP (First Contentful Paint) | Довше показується перший контент |
| LCP (Largest Contentful Paint) | Основний елемент відображається пізніше |
| TTI (Time to Interactive) | JS блокує головний потік |
| TBT (Total Blocking Time) | Зростає через важкі синхронні скрипти |
Як робити в продакшені
| Тип скрипта | Де і як підключати |
|---|---|
| Основна логіка (UI, SPA) | <script src="main.js" defer> |
| Аналітика, реклама | <script src="metrics.js" async> |
| Inline-ініціалізація | <script> усередині <head>, якщо ініціалізація справді маленька |
| Старі бібліотеки | Краще перенести в кінець <body> |
Підсумкова таблиця:
| Сценарій | Що робить | Наслідок |
|---|---|---|
<script> у <head> без атрибутів | Блокує парсинг | Повільно |
<script defer> | Завантажує паралельно, виконує після DOM | Оптимально |
<script async> | Завантажує й виконує незалежно | Лише для незалежних скриптів |
<script> унизу <body> | Чекає кінця HTML | Без блокування |
Типові помилки
- Вважати, що проблема в самому
<head>. Блокує не розташування, а синхронне виконання:<script defer>у<head>, це найкращий варіант. - Ставити
asyncна код, який залежить від DOM або від іншого файлу. Він може виконатися до появи елементів і зламатися випадковим чином. - Ховати проблему за
window.onload. Скрипт усе одно завантажується синхронно і затримує парсинг, просто логіка всередині чекає довше. - Забувати про блокувальний CSS. Важкий
<link rel="stylesheet">теж затримує рендер, тому критичні стилі варто інлайнити. - Класти в
<head>величезний inline-скрипт. Мережа тут ні до чого, але парсинг і виконання все одно займають головний потік. - Підключати сторонні віджети без
async. Повільний чужий сервер тоді безпосередньо затримує рендер вашої сторінки.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.