Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Парсинг пайплайн - від байтів до DOM та CSSOM». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Parsing pipeline** - це процес, яким браузер перетворює сирі байти HTML та CSS на дерева DOM і CSSOM. Чотири етапи: Байти → Символи (декодування charset) → Токени (StartTag, EndTag, Character) → Дерева (DOM з HTML, CSSOM з CSS). ``` Bytes → Characters → Tokens → DOM / CSSOM ``` **Ключове:** Парсинг інкрементальний. CSS блокує рендеринг до готовності CSSOM. Скрипти блокують парсинг без `async` або `defer`.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Parsing pipeline** - це процес, яким браузер перетворює сирі байти HTML та CSS на дерева DOM і CSSOM, з якими можуть працювати JavaScript і рушій рендерингу. ## Теорія ### TL;DR - Пайплайн має 4 етапи: Байти → Символи → Токени → Дерево - HTML і CSS проходять ті самі етапи, але дають різні результати: живий і змінний DOM проти read-only CSSOM - Парсинг інкрементальний - браузер починає рендеринг до того, як завантажиться весь документ - Тег `<script>` зупиняє HTML-парсер; CSS в `<head>` блокує рендеринг до готовності CSSOM - `defer` завантажує паралельно і виконується після парсингу; `async` виконується одразу як файл завантажився ### Швидкий приклад ```javascript // Браузер отримує HTML як сирі байти // 3C 68 31 3E 48 65 6C 6C 6F 3C 2F 68 31 3E // Етап 1: Байти → Символи (UTF-8 декодування) // "<h1>Hello</h1>" // Етап 2: Символи → Токени // StartTag(h1), Character("Hello"), EndTag(h1) // Етап 3: Токени → DOM вузол document.querySelector('h1').textContent; // "Hello" // Вузол вже існує до того, як завантажиться решта документа ``` Парсинг не чекає на весь файл. Браузер обробляє шматки по ~14KB і одразу будує дерево. ### Чотири етапи **Етап 1: Байти в символи.** Перш ніж зчитати хоч один символ, браузеру потрібно знати кодування. Він перевіряє по черзі: BOM (Byte Order Mark) у файлі, HTTP заголовок `Content-Type`, тег `<meta charset>`, потім авто-визначення. Перший збіг виграє. Тому `<meta charset="utf-8">` має бути в перших 1024 байтах. Якщо браузер вже почав парсити з неправильним кодуванням - він перезапускається з початку. **Етап 2: Токенізація (tokenization).** Токенізатор - це скінченний автомат із HTML-специфікації. Він читає потік символів і видає токени: `StartTag`, `EndTag`, `Character`, `Comment`. Цікавий момент: `<tr>` без `<tbody>` не викликає помилку. Токенізатор просто видає токени, а будівник дерева виправляє структуру автоматично. **Етап 3: Побудова дерева.** Будівник дерева споживає токени і створює DOM-вузли. Він спокійно обробляє некоректний HTML: автоматично закриває відкриті теги, вставляє пропущені елементи на кшталт `<tbody>`, переміщує вузли відповідно до правил HTML5. DOM живий протягом усього процесу. JavaScript, що виконується під час парсингу, вже бачить вузли, які були створені. **Етап 4: CSS іде паралельним шляхом.** Поки HTML-парсинг будує DOM, CSS-файли мають власний токенізатор і будівник дерева. CSS-токенізатор видає токени для селекторів, властивостей і значень. Будівник дерева будує CSSOM. На відміну від HTML, невалідний CSS просто ігнорується, не зупиняючи парсер. Але рендеринг заблокований до готовності і DOM, і CSSOM. Саме тому CSS має бути в `<head>`. ### Preload scanner Більшість розробників про це не думають. Головний HTML-парсер зупиняється на тезі `<script>`. Але браузер не просто чекає. Паралельний потік, який називають preload scanner, продовжує читати сирий HTML вперед і ставить у чергу завантаження ресурсів: `<link rel="stylesheet">`, `<script src>`, `<img src>`, `<link rel="preload">`. ```html <script src="slow-script.js"></script> <!-- Парсер заблокований тут --> <img src="hero.jpg"> <!-- Preload scanner вже почав завантажувати hero.jpg --> ``` Це одна з найвпливовіших оптимізацій браузера. `document.write()` ламає її повністю, бо може вставити нові символи в потік після того, як сканер вже пройшов далі. ### Головна різниця: DOM vs CSSOM DOM можна змінювати. JavaScript читає і пише його постійно. CSSOM з точки зору JavaScript - read-only. Читати обчислені стилі можна через `getComputedStyle()`, але писати в CSSOM напряму не можна. Виклик `element.style.color = 'red'` не торкається CSSOM. Він створює inline-стиль на DOM-вузлі, який перемагає правила CSSOM через найвищу специфічність. ### Коли це важливо - CSS в `<body>` замість `<head>` затримує перший paint, бо CSSOM не готовий коли DOM вже є - `<script>` без `async` або `defer` блокує і парсинг, і рендеринг - `<meta charset>` після перших 1024 байтів може змусити браузер перезапустити парсинг - Великі блоки inline-стилів роздувають DOM і сповільнюють побудову дерева - `document.write()` вимикає preload scanner і вбиває паралельне завантаження ### Типові помилки **Помилка 1: Звернення до DOM до того, як він існує** ```javascript // НЕПРАВИЛЬНО: скрипт виконується до парсингу <h1> <script> console.log(document.querySelector('h1')); // null </script> <h1>Hello</h1> // ПРАВИЛЬНО: чекаємо DOMContentLoaded document.addEventListener('DOMContentLoaded', () => { console.log(document.querySelector('h1')); // "Hello" }); ``` Парсер зупиняється на `<script>`, виконує його, потім продовжує. Якщо `<h1>` нижче за тег скрипту, вузол ще не існує під час виконання. **Помилка 2: CSS в body** ```html <!-- НЕПРАВИЛЬНО --> <body> <h1>Заголовок</h1> <link rel="stylesheet" href="styles.css"> <!-- CSSOM не готовий --> <p>Контент</p> </body> <!-- ПРАВИЛЬНО --> <head> <link rel="stylesheet" href="styles.css"> </head> ``` Браузер нічого не малює, поки не готові і DOM, і CSSOM. CSS в body означає, що CSSOM може не бути готовим, коли парсер дійшов до видимого контенту вище нього. **Помилка 3: Спроба записати в getComputedStyle()** ```javascript // НЕПРАВИЛЬНО: CSSOM read-only const style = getComputedStyle(element); style.color = 'red'; // нічого не відбудеться // ПРАВИЛЬНО element.style.color = 'red'; // inline стиль element.classList.add('highlight'); // застосувати правило з CSSOM ``` **Помилка 4: async для скриптів, що залежать один від одного** ```html <!-- НЕПРАВИЛЬНО: порядок виконання не гарантований --> <script src="jquery.js" async></script> <script src="app.js" async></script> <!-- app.js може виконатися до jquery.js --> <!-- ПРАВИЛЬНО: defer зберігає порядок --> <script src="jquery.js" defer></script> <script src="app.js" defer></script> ``` `async` запускає скрипт одразу як він завантажився, ігноруючи порядок у документі. `defer` чекає завершення парсингу, потім виконує скрипти по черзі. Для коду, що використовує DOM, `defer` - майже завжди правильний вибір. **Помилка 5: Розрахунок на правильне авто-визначення кодування** ```html <!-- НЕПРАВИЛЬНО: мета-тег надто пізно --> <html> <head> <title>Сторінка</title> <!-- ~800 байт іншого вмісту --> <meta charset="utf-8"> <!-- браузер міг уже помилитися з кодуванням --> </head> ``` Декларація charset має бути в перших 1024 байтах. Якщо ні - браузер угадує. Якщо угадав неправильно - символи розсипаються і парсер перезапускається з початку. ### Де зустрічається в реальному коді - **React SSR:** `renderToString()` надсилає HTML-байти з Node.js. Браузер парсить їх за тим самим пайплайном. Hydration відбувається після побудови DOM, тому `useEffect` запускається після paint - **Next.js:** Стримить HTML через `renderToNodeStream()`, що дозволяє браузеру починати парсинг до завершення відповіді. Це інкрементальний рендеринг у дії - **Chrome DevTools:** Метрика "Parse HTML" у вкладці Performance вимірює токенізацію і побудову дерева. Великі значення зазвичай означають або дуже великий документ, або некоректний HTML, який парсер виправляє - **Webpack/бандлери:** Управляють завантаженням скриптів через атрибути `async`, `defer` або `type="module"` на тегах `<script>` - **Googlebot:** Парсить HTML і виконує JavaScript для побудови DOM, після чого індексує результат. Рендеринг тільки на клієнті може приховати контент від краулера ### Follow-up питання **Q:** Чому браузер парсить HTML інкрементально, а не чекає на весь файл? **A:** Продуктивність. Якщо чекати завантаження 5MB HTML перед початком парсингу, користувачі бачать порожній екран секундами. Інкрементальний парсинг дає змогу рендерити контент вище згину, поки решта документа ще в дорозі. **Q:** Що буде, якщо `<meta charset>` суперечить HTTP заголовку `Content-Type`? **A:** Перемагає HTTP заголовок. Браузер обробляє charset з `Content-Type: text/html; charset=utf-8` до того, як прочитає хоч один байт HTML. Тег `<meta>` має значення тільки коли заголовок відсутній. **Q:** Чому JavaScript не може напряму змінити CSSOM? **A:** CSSOM індексований по селектору для швидкого пошуку, а не для мутацій. Прямий запис також непередбачувано ламав би каскад стилів. Натомість змінюй DOM (inline стилі або класи), і браузер перераховує обчислені стилі з урахуванням CSSOM. **Q:** Чим `defer` відрізняється від `async` з точки зору парсингу? **A:** Обидва завантажують скрипт паралельно, не блокуючи парсер. Різниця в моменті виконання. `async` запускає скрипт одразу як він завантажився, потенційно перериваючи парсинг. `defer` чекає завершення парсингу і запускає скрипти в порядку їх появи у документі. **Q:** (Senior) Є 50MB HTML з тисячами inline-стилів. Як зменшити час парсингу? **A:** Для початку - не надсилати 50MB HTML. Розбий сторінку і завантажуй секції лінивим завантаженням. Якщо все ж необхідно: перенеси inline-стилі в CSS-класи, бо вони роздувають і DOM, і запускають перерахунок CSSOM при кожній зміні стилів. Використай `Transfer-Encoding: chunked` або `renderToNodeStream()` у React, щоб браузер починав парсинг до завершення відповіді. Потім профілюй у Chrome DevTools - найчастіше справжнє вузьке місце не парсинг, а layout і paint. ## Приклади ### Базовий приклад: порядок визначення кодування ```html <!DOCTYPE html> <!-- Браузер перевіряє кодування в такому порядку: --> <!-- 1. BOM у файлі (найвищий пріоритет) --> <!-- 2. HTTP Content-Type: text/html; charset=utf-8 --> <!-- 3. Цей мета-тег (має бути в перших 1024 байтах) --> <meta charset="utf-8"> <title>Сторінка</title> <!-- 4. Авто-визначення браузером (запасний варіант) --> ``` Визначення charset відбувається до токенізації першого символу. Якщо перші три сигнали суперечать один одному - виграє раніший, і парсер може перезапуститися. ### Середній рівень: блокування, defer та async ```html <!-- Блокує парсинг - парсер чекає завантаження і виконання --> <script src="app.js"></script> <!-- Завантажує паралельно, виконується ПІСЛЯ парсингу, по порядку --> <script src="jquery.js" defer></script> <script src="app.js" defer></script> <!-- Завантажує паралельно, виконується одразу як готовий, порядок НЕ гарантований --> <script src="analytics.js" async></script> <!-- Вимикає preload scanner для всієї сторінки --> <script>document.write('<script src="bad.js"><\/script>');</script> ``` Для скриптів, що торкаються DOM або залежать один від одного, `defer` - правильний вибір. `async` підходить для ізольованих скриптів, як аналітика, яка не залежить від стану сторінки. ### Просунутий рівень: FOUC через порядок завантаження CSS ```javascript // Сервер надсилає цей HTML (Node.js / React SSR) const html = ` <!DOCTYPE html> <html> <head> <!-- Якщо styles.css завантажується повільно, браузер покаже порожній екран поки CSSOM не готовий --> <link rel="stylesheet" href="styles.css"> </head> <body> <h1>Контент</h1> </body> </html>`; // Браузер будує DOM інкрементально. // Рендеринг чекає на CSSOM. // Повільний styles.css = порожній екран, не нестилізований контент. // FOUC виникає коли CSS в <body> або завантажується через JS після побудови DOM. // Рішення: inline критичний CSS в <head> для контенту вище згину const criticalCss = `h1 { font-size: 2rem; }`; const optimized = ` <head> <style>${criticalCss}</style> <link rel="stylesheet" href="styles.css" media="print" onload="this.media='all'"> </head>`; ``` FOUC (Flash of Unstyled Content, миготіння нестилізованого контенту) виникає коли DOM готовий, а CSSOM ще ні. Inline критичних стилів прибирає блокуючий мережевий запит для контенту вище згину.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.