Skip to main content

Скрипти в 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> допустимий, якщо він справді маленький.

Швидкий приклад

html
<head> <script src="main.js"></script> </head> <body> <h1>Привіт!</h1> </body>

Покроково:

  1. Браузер починає читати HTML.
  2. Доходить до <script src="main.js">.
  3. Зупиняє парсинг HTML.
  4. Чекає, поки main.js завантажиться по мережі, а це можуть бути сотні мілісекунд.
  5. Виконує скрипт.
  6. І лише потім продовжує парсити решту сторінки.

Увесь цей час DOM ще не створено, користувач не бачить контенту, а метрики FCP і LCP страждають.

Як браузер завантажує сторінку

Коли браузер отримує HTML-документ, він виконує такі кроки:

  1. Парсить HTML рядок за рядком, згори вниз.
  2. Коли зустрічає тег <link> з CSS, завантажує стиль, бо стилі впливають на відображення.
  3. Коли зустрічає тег <script>, зупиняє парсинг, щоб виконати JS.

Чому так? Тому що скрипт може змінити сам DOM або навіть видалити елементи, які йдуть далі в HTML (document.write, innerHTML тощо). Тому браузер зобов'язаний дочекатися виконання JS, перш ніж продовжити розбір.

Схематично синхронний варіант виглядає так:

text
HTML: <head> -> <script> -> <body> | завантаження [======= JS завантажується =======] | виконання [======= JS виконується =======] | і лише потім будується DOM

А з defer завантаження йде паралельно, а виконання відкладається на кінець:

text
HTML і JS завантажуються разом -> DOM побудовано -> JS виконується

Які проблеми це спричиняє

ПроблемаЩо відбувається
Блокується HTML-парсерСторінка не будується, доки не завантажиться JS
Довга «біла сторінка»Користувач бачить порожнечу
Повільні мережі, гірший UXОсобливо на 3G і 4G
Погіршення Core Web VitalsLCP, FCP, TTI, TBT зростають
Скрипти заважають критичному CSSКритичні стилі застосовуються невчасно

Як уникнути блокування

Спосіб 1. defer

html
<script src="main.js" defer></script>

Скрипт завантажується паралельно з HTML, але виконується лише після повного парсингу DOM, перед подією DOMContentLoaded. Без блокування, з гарантованим порядком виконання, і це ідеальний варіант для більшості випадків.

Спосіб 2. async

html
<script src="analytics.js" async></script>

Скрипт завантажується асинхронно, у фоні, і виконується одразу після завантаження, не чекаючи інших. Використовується для незалежних скриптів: аналітики, реклами, пікселів. Порядок виконання не гарантується.

Спосіб 3. Перенести скрипти в кінець <body>

html
<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. Повільний чужий сервер тоді безпосередньо затримує рендер вашої сторінки.

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

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

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