Вебплатформа
Що браузер робить зі сторінкою незалежно від фреймворку: як розкладає блоки, як робить інтерфейс доступним, як перетворює HTML на пікселі і як це пришвидшити. Angular і React працюють поверх цього шару, тому на fullstack-співбесіді його перевіряють окремо.
Перед цією темою: JavaScript і TypeScript.
Як влаштована верстка в CSS: блокова модель, flexbox і grid?
Section titled “Як влаштована верстка в CSS: блокова модель, flexbox і grid?”Що це
- Блокова модель. Елемент — це content, padding, border і margin. За замовчуванням
widthне включає padding і border. display.blockзаймає весь рядок,inlineтече в тексті,flexіgridрозкладають дітей елемента.- Flexbox ставить дітей у рядок або колонку, grid — у сітку рядків і колонок.
Як користуватися
- Для всіх елементів —
box-sizing: border-box. - Каркас сторінки — grid, елементи всередині блока — flex, відстані —
gap. - Стилі за замовчуванням — для телефона, для широких екранів —
@media (min-width: 768px). Префіксmd:у Tailwind працює так само.
Типова помилка
Розкладати блоки через float і position: absolute там, де вистачає flex або grid. Така верстка ламається від першого довгого тексту.
Як влаштовано
- Каскад. Якщо до елемента підходять кілька правил, перемагає те, що вище за списком:
!important, inline-стиль, правило поза@layerабо в пізнішому шарі, більша специфічність, пізніше місце в коді. - Специфічність — трійка: id; класи, атрибути й псевдокласи; теги й псевдоелементи. Порівнюється зліва направо, тому
#nav a(1,0,1) сильніший за.menu .item a(0,2,1). - Позиціювання.
relativeзсуває елемент, зберігаючи його місце.absoluteвиймає його з потоку й рахує координати від найближчого позиціонованого предка.fixed— від вікна,stickyприлипає під час прокручування.
Коли що обирати
| Flexbox | Grid | |
|---|---|---|
| Осі | одна: рядок або колонка | дві: рядки й колонки |
| Розмір задає | вміст | контейнер |
| Приклад | меню, кнопки в ряд, картка | каркас сторінки, галерея, форма |
Пастки
- Схлопування margin. Вертикальні margin сусідніх блоків у звичайному потоці не додаються, береться більший. У flex і grid цього немає.
z-index«не працює». Він порівнюється всередині одного контексту накладання, а його створюють, наприклад,opacityменше 1 абоtransformу предка.- Flex-елемент не стискається. Мінімальна ширина flex-елемента за замовчуванням — ширина його вмісту; довге слово розпирає колонку, допомагає
min-width: 0.
Правило
Grid — для каркаса, flex — для компонентів, gap — замість margin між сусідами, медіазапити — mobile-first.
Під капотом
- Flex-алгоритм спершу бере базовий розмір елементів (
flex-basis, інакше за вмістом), потім роздає вільне місце заflex-growабо забирає заflex-shrink. Тому колонки з різним текстом виходять різної ширини. - Grid спершу будує доріжки з
grid-template-columns, аfrділить решту місця.repeat(auto-fit, minmax(12rem, 1fr))дає адаптивну галерею без медіазапитів. container-type: inline-sizeвмикає size containment: ширина контейнера не залежить від вмісту. Інакше виник би цикл «стиль залежить від розміру, розмір — від стилю».
Компроміси
- Медіазапит чи container query. Медіазапит підходить для каркаса сторінки. Картка, яка стоїть і в сайдбарі, і в основній колонці, має реагувати на свій контейнер. Container queries — Baseline з 2023 року, у Tailwind v4 вони вбудовані (
@container,@md:). - Одиниці.
remу шрифтах поважає налаштування розміру шрифту користувача,px— ні.font-sizeлише уvwне зростає під час масштабування сторінки, тому вclamp()додаютьrem. На мобільних100vhвключає область під панеллю браузера, точніше100dvh. - Керування каскадом.
@layer reset, library, appфіксує порядок шарів, а стилі поза шарами сильніші за будь-який шар.:where()обнуляє специфічність базових стилів, і їх легко перевизначити. Утилітарний CSS на кшталт Tailwind і CSS-модулі знімають перегони, бо класи однієї ваги.
Як перевірити в продакшені
- DevTools → Elements → Computed показує, яке правило перемогло, а закреслені — ті, що програли. Значки
flexіgridу дереві елементів вмикають оверлей сітки. - Скриншотні тести Playwright на кількох ширинах ловлять зламану верстку до релізу.
Підсумок: grid задає розмір згори, flex — знизу; адаптивність компонента — через його контейнер, а пріоритети стилів — через шари, а не !important.
*, *::before, *::after { box-sizing: border-box; }
/* Каркас: mobile-first — одна колонка */.page { display: grid; grid-template-columns: 1fr; gap: 1rem;}@media (min-width: 768px) { .page { grid-template-columns: 16rem 1fr; } /* сайдбар + контент */}
/* Галерея без медіазапитів: стільки колонок, скільки вміститься */.gallery { display: grid; grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); gap: 1rem;}
/* Картка підлаштовується під свій контейнер, а не під вікно */.card-slot { container-type: inline-size; }.card { display: flex; flex-direction: column; gap: 0.5rem; }@container (min-width: 30rem) { .card { flex-direction: row; } /* картинка ліворуч, текст праворуч */}
/* Від 24px до 40px за кореневого шрифту 16px */h1 { font-size: clamp(1.5rem, 1rem + 2vw, 2.5rem); }- Чим
display: noneвідрізняється відvisibility: hidden? — Перший прибирає елемент із розкладки, другий ховає, але залишає його місце. - Що означає
flex: 1? —flex-grow: 1,flex-shrink: 1,flex-basis: 0%: елементи ділять вільне місце порівну. - Чим
auto-fitвідрізняється відauto-fillу grid? —auto-fitсхлопує порожні доріжки й розтягує елементи,auto-fillзалишає порожні доріжки. - Від чого рахує координати
position: absolute? — Від найближчого предка зpositionнеstatic, інакше від початкового блока-контейнера. - Яка специфічність у
:is()і:where()? —:is()— як у найсильнішого аргументу,:where()— завжди нуль.
Як зробити інтерфейс доступним?
Section titled “Як зробити інтерфейс доступним?”Що це
Доступність — інтерфейсом можуть користуватися люди з порушеннями зору, слуху й моторики: зі скринрідером, лише з клавіатури, зі збільшеним шрифтом.
Як користуватися
- Дія —
<button>, перехід —<a href>, а неdivз обробником кліку: вони фокусуються й натискаються з клавіатури. - У кожного поля —
<label for="...">. Плейсхолдер підписом не вважається: він зникає під час введення. - У картинки —
altз її змістом, у декоративної — порожнійalt="". - Заголовки
h1–h6ідуть по порядку, області розміченіheader,nav,main. - Контраст звичайного тексту з тлом — не менше 4,5:1.
Типова помилка
Прибрати рамку фокуса через outline: none, бо «некрасиво». Користувач клавіатури перестає бачити, де він. Рамку не прибирають, а оформлюють через :focus-visible.
Як влаштовано
- З DOM і CSS браузер будує дерево доступності: роль (
button), доступне ім’я («Зберегти») і стан (disabled,expanded). - Скринрідер — NVDA, JAWS, VoiceOver, TalkBack — отримує це дерево через API доступності ОС і озвучує.
- ARIA змінює лише дерево, але не поведінку:
role="button"наdivне дасть ні фокуса, ні реакції на Enter і пробіл.
Коли що обирати
| Задача | Рішення |
|---|---|
| Поле без видимого підпису | aria-label або aria-labelledby |
| Помилка поля | aria-invalid="true" і aria-describedby на текст помилки |
| Повідомлення після дії | live region: role="status" або role="alert" |
| Модальне вікно | <dialog> і showModal(): тло недоступне, Esc закриває |
| Стан віджета | aria-expanded, aria-selected, aria-current |
Пастки
- ARIA поверх нативного. Перше правило ARIA: якщо є HTML-елемент із потрібною семантикою й поведінкою, беріть його.
tabindexбільше нуля ламає природний порядок обходу. Використовують лише0і-1.- Зміна маршруту в SPA. Фокус і озвучення не змінюються самі, їх переводять на заголовок нової сторінки вручну.
- Лише колір. Помилку, виділену тільки червоною рамкою, не побачить дальтонік і не озвучить скринрідер — потрібен текст.
Правило
Спершу семантичний HTML, потім ARIA для відсутнього; кожен сценарій проходять із клавіатури: Tab, Enter, пробіл, Esc, стрілки.
Стандарти й закон
- WCAG 2.2 (W3C, жовтень 2023) — критерії трьох рівнів: A — мінімум, AA — мета законів і контрактів, AAA — для окремих випадків.
- Нове у 2.2 на рівні AA: фокус не перекритий липкими панелями (2.4.11), ціль дотику не менше 24×24 CSS-пікселів (2.5.8), альтернатива перетягуванню (2.5.7), вхід без когнітивного тесту — наприклад, вставлення пароля з менеджера (3.3.8). Критерій 4.1.1 Parsing вилучено.
- EAA (директива 2019/882) застосовується з 28 червня 2025 року до продуктів і послуг на ринку ЄС: інтернет-магазини, банки, транспорт, електронні книги. Мікропідприємства, що надають послуги, звільнені. Технічна опора — стандарт EN 301 549, який посилається на WCAG 2.1 AA.
Компроміси
- Свій віджет чи готовий. Combobox, вкладки або дерево на ARIA — це ролі, стани й клавіатурна модель з ARIA Authoring Practices. Зазвичай дешевше взяти перевірену бібліотеку або Angular CDK
a11y, ніж писати своє. - Автоматика чи руки. axe-core і Lighthouse знаходять низький контраст, порожні підписи й неправильні ролі. Чи зрозумілий порядок фокуса і чи змістовний
alt, вони не скажуть.
Як перевірити в продакшені
- axe-core в e2e-тестах (
@axe-core/playwright) і Lighthouse CI в пайплайні ловлять регресії. - Вручну: основний сценарій проходять лише з клавіатури та зі скринрідером — NVDA на Windows, VoiceOver на macOS.
- DevTools → Elements → Accessibility показує роль, ім’я і стан вузла та все дерево доступності.
Підсумок: мета — WCAG 2.2 AA, закріплена в компонентах дизайн-системи та в автотестах, а ручна перевірка закриває те, чого автоматика не бачить.
<form novalidate> <!-- for/id пов'язує підпис із полем: клік по підпису ставить фокус у поле --> <label for="email">Email</label> <input id="email" type="email" autocomplete="email" aria-invalid="true" aria-describedby="email-error"> <!-- Скринрідер прочитає текст помилки разом із полем --> <p id="email-error">Enter a valid email</p>
<!-- Кнопка-іконка: доступне ім'я через aria-label, іконку ховаємо --> <button type="button" aria-label="Close"> <svg aria-hidden="true" width="16" height="16"><path d="M2 2l12 12M14 2L2 14"/></svg> </button>
<img src="chart.png" alt="Sales grew 20% in Q3"> <img src="divider.png" alt=""> <!-- декоративна: скринрідер пропустить -->
<!-- Live region: текст, вставлений сюди, буде озвучено --> <div role="status"></div>
<button type="submit">Save</button></form>/* Рамка лише під час навігації з клавіатури, а не під час кліку мишею */button:focus-visible,a:focus-visible { outline: 3px solid #1a73e8; outline-offset: 2px;}- Що таке перше правило ARIA? — Якщо нативний HTML-елемент дає потрібну семантику й поведінку, використовуйте його, а не роль на
div. - Чим
aria-labelвідрізняється відaria-labelledby? — Перший задає ім’я рядком, другий посилається на id видимого тексту. - Чим
role="alert"відрізняється відrole="status"? —alertозвучується одразу, перериваючи читання,status— ввічливо, після поточної фрази. - Який контраст вимагає WCAG AA? — 4,5:1 для звичайного тексту, 3:1 для великого й для елементів інтерфейсу.
- Що змінив Європейський акт про доступність? — З 28 червня 2025 року доступність обов’язкова для багатьох цифрових послуг в ЄС, орієнтир — EN 301 549 і WCAG.
Як браузер перетворює HTML на пікселі?
Section titled “Як браузер перетворює HTML на пікселі?”Що це
Шлях від HTML до пікселів:
- DOM. HTML розбирається в дерево вузлів.
- CSSOM. CSS розбирається в дерево правил.
- Дерево відмальовування (render tree). Лише видимі елементи з підсумковими стилями:
display: noneсюди не потрапляє. - Layout. Розрахунок розмірів і координат кожного блока.
- Paint. Заливання пікселів: текст, кольори, тіні, картинки.
- Composite. Збирання шарів у підсумковий кадр.
Як користуватися
- CSS — у
<head>, скрипти — з атрибутомdefer. - Анімувати
transformіopacity, а неtop,leftчиwidth.
Типова помилка
Підключити великий скрипт у <head> без defer. Поки він завантажується й виконується, HTML не розбирається, і екран лишається білим.
Як влаштовано
- CSS блокує відмальовування. Без CSSOM браузер не малює, інакше користувач побачив би нестилізовану сторінку. Синхронний скрипт ще й чекає на стилі перед собою: він може їх читати.
- JS блокує розбір. Звичайний
<script>зупиняє парсер до завантаження й виконання. Тим часом preload scanner переглядає HTML далі й заздалегідь починає вантажити картинки та стилі. - JS і відмальовування ділять головний потік. Кадр малюється між завданнями, тому довгий обробник його відкладає. Як влаштований event loop — у темі JavaScript і TypeScript.
Коли що обирати
Атрибут <script> |
Розбір HTML | Виконання |
|---|---|---|
| немає | зупиняється | одразу, по порядку |
async |
триває | щойно завантажиться, без порядку |
defer |
триває | після розбору, по порядку, до DOMContentLoaded |
type="module" |
триває | як defer за замовчуванням |
Скільки коштує зміна
| Зміна | Етапи |
|---|---|
width, top, font-size, нові вузли |
layout, paint, composite (reflow) |
color, background, box-shadow |
paint, composite (repaint) |
transform, opacity |
лише composite |
Пастки
- Layout thrashing. Запис стилю, а одразу за ним читання
offsetHeightчиgetBoundingClientRect()змушують перерахувати layout негайно; у циклі — на кожній ітерації. asyncдля залежних скриптів. Порядок не гарантований: бібліотека може завантажитися пізніше за код, який її використовує.
Правило
Скрипти — defer або модулі, читання layout — пачкою до записів, анімації — на transform і opacity.
Під капотом
- У Chromium етап paint лише записує команди відмальовування. Растеризацію та збирання кадру виконують потік композитора і GPU, а не головний потік.
- Анімація
transformіopacityна окремому шарі повністю йде в композиторі, тому вона плавна, навіть коли головний потік зайнятий. Шар створюють, наприклад,will-change: transform,<video>і<canvas>. - Бюджет кадру за 60 Гц — близько 16,7 мс на все: JS, стилі, layout і paint.
Ціна
- Forced synchronous layout у циклі по N елементах — N перерахунків layout замість одного.
- Кожен шар — текстура в пам’яті GPU. Сотні шарів на слабкому телефоні гірші за зайвий repaint.
- Великий DOM здорожує кожен перерахунок стилів і layout.
Компроміси
- Читання й записи DOM розділяють: спершу всі вимірювання, потім усі зміни, візуальні зміни — у
requestAnimationFrame. content-visibility: autoпропускає рендеринг блоків поза екраном, але їм потрібенcontain-intrinsic-size, інакше стрибає смуга прокручування.- Критичний CSS, вбудований у
<head>, пришвидшує перший кадр, але не кешується окремо й збільшує кожну HTML-відповідь.
Як перевірити в продакшені
- DevTools → Performance: фіолетові блоки Layout і Recalculate Style всередині довгих завдань, а insight Forced reflow вказує на рядок коду.
- Вкладка Rendering → Paint flashing підсвічує перемальовані області, панель Layers показує шари.
- У полі — Long Animation Frames API (Chromium): у кожного скрипта є
forcedStyleAndLayoutDuration.
Підсумок: головний потік один на JS і рендеринг: не змушуйте його перераховувати layout синхронно й віддавайте анімації композитору.
<head> <!-- CSS блокує відмальовування: тримаємо його невеликим і в head --> <link rel="stylesheet" href="/app.css"> <!-- media="print": на екрані показ не блокує --> <link rel="stylesheet" href="/print.css" media="print">
<script src="/legacy.js"></script> <!-- зупиняє розбір HTML --> <script defer src="/app.js"></script> <!-- після розбору, по порядку --> <script async src="/analytics.js"></script> <!-- щойно завантажиться, без порядку --> <script type="module" src="/main.js"></script> <!-- відкладений, як defer --></head>const items = document.querySelectorAll('.item');
// Погано: читання після запису на кожній ітерації — layout перераховується N разівfor (const item of items) { item.style.width = item.parentElement.offsetWidth / 2 + 'px';}
// Добре: спершу всі читання, потім усі записи — один перерахунокconst widths = [...items].map((item) => item.parentElement.offsetWidth);requestAnimationFrame(() => { items.forEach((item, i) => { item.style.width = widths[i] / 2 + 'px'; });});/* Анімація лише на композиторі: без layout і paint */.toast { transition: transform 200ms, opacity 200ms; }.toast.hidden { transform: translateY(100%); opacity: 0; }- Чим reflow відрізняється від repaint? — Reflow перераховує геометрію й тягне за собою paint, repaint лише перемальовує пікселі.
- Чому
display: noneіvisibility: hiddenпо-різному впливають на layout? — Перший прибирає елемент із дерева відмальовування, другий залишає його місце. - Коли спрацьовує
DOMContentLoaded, а колиload? —DOMContentLoaded— після розбору HTML іdefer-скриптів,load— після всіх картинок і стилів. - Чи блокує відмальовування
<link rel="stylesheet" media="print">? — Ні, на екрані він не потрібен; файл однаково завантажиться з низьким пріоритетом. - Що робить preload scanner? — Поки парсер стоїть на скрипті, він переглядає HTML далі й заздалегідь запускає завантаження ресурсів.
Як виміряти й пришвидшити завантаження вебзастосунку?
Section titled “Як виміряти й пришвидшити завантаження вебзастосунку?”Що це
| Метрика | Що вимірює | Добре |
|---|---|---|
| LCP | коли відмальовано найбільший елемент першого екрана | до 2,5 с |
| INP | затримку від кліку чи натискання клавіші до наступного кадру | до 200 мс |
| CLS | суму неочікуваних зсувів верстки | до 0,1 |
Як користуватися
- Виміряйте: Lighthouse у DevTools або PageSpeed Insights.
- Задайте картинкам
widthіheight: браузер заздалегідь резервує місце, і верстка не стрибає. - Картинкам нижче першого екрана —
loading="lazy". - Важкі сторінки завантажуйте під час переходу на них: ліниві маршрути в Angular,
React.lazyі динамічнийimport().
Типова помилка
Поставити loading="lazy" на головну картинку першого екрана. Вона почне завантажуватися пізніше, і LCP зросте.
Як влаштовано
| Метрика | Добре | Погано |
|---|---|---|
| LCP | до 2,5 с | більше 4 с |
| INP | до 200 мс | більше 500 мс |
| CLS | до 0,1 | більше 0,25 |
Оцінка — 75-й перцентиль візитів, окремо для мобільних і десктопа. INP замінив FID у березні 2024 року: FID вимірював лише затримку першого введення, INP враховує всі кліки, дотики й натискання клавіш.
Лабораторія і поле
| Лабораторні дані | Польові дані | |
|---|---|---|
| Джерело | Lighthouse, DevTools Performance | CrUX, бібліотека web-vitals, RUM |
| Умови | один пристрій, задана мережа | реальні пристрої й мережі |
| Для чого | налагодження, регресії в CI | що бачать користувачі |
Lighthouse під час завантаження сторінки INP не вимірює: у лабораторії його замінює Total Blocking Time.
Пастки
- Зелений Lighthouse на потужному ноутбуці за поганих польових даних із телефонів.
preloadдля всього підряд: ресурси конкурують за канал, і важливе вантажиться пізніше.
Правило
LCP — картинка в HTML із fetchpriority="high", INP — менше JS і коротші завдання, CLS — розміри в усього, що завантажується пізніше. Проблему шукають у полі, причину — в лабораторії.
Під капотом
- LCP = час до першого байта + затримка початку завантаження ресурсу + тривалість завантаження + затримка відмальовування. Майже весь час має йти на завантаження HTML і самого ресурсу.
- INP = затримка введення + обробка + затримка до відмальовування. Береться найгірша взаємодія візиту, а за великої їх кількості одна найгірша на кожні 50 відкидається.
- HTTP/2 мультиплексує запити в одному з’єднанні, тому склеювати все в один файл більше не потрібно. HTTP/3 працює поверх QUIC і за втрати пакета не блокує решту потоків.
Ціна
- Кілобайт JS дорожчий за кілобайт картинки: його треба розібрати, скомпілювати й виконати в головному потоці.
- Шрифти: WOFF2 і підмножина символів.
font-display: swapпоказує текст одразу, але зі зсувом під час підміни шрифту. - Статика з хешем в імені кешується на рік (
Cache-Control: max-age=31536000, immutable), HTML —no-cache. Серверний бік — у темі Фонові задачі й кешування, CDN — у System design.
Компроміси
- SSR і гідратація. Сервер віддає готовий HTML, і LCP настає раніше. Але клієнт однаково завантажує JS і навішує обробники, і до кінця гідратації сторінка видима, але не відповідає. Детально — у темі React і Next.js.
- Код-сплітинг. Стартовий бандл менший, але перехід на лінивий маршрут чекає на завантаження чанка. Допомагає попереднє завантаження в простої або під час наведення.
preconnectіpreload. Перший заздалегідь відкриває з’єднання з іншим доменом, другий вантажить ресурс, який браузер знаходить пізно. Зайві підказки забирають канал у важливого.
Як перевірити в продакшені
- RUM: бібліотека
web-vitalsнадсилає LCP, INP і CLS з атрибуцією у вашу аналітику; CrUX і Search Console показують підсумок за 28 днів. - Бюджети в CI:
budgetsвangular.json, Lighthouse CI з порогами, аналіз бандла черезsource-map-explorer.
Підсумок: вимірюйте в полі, розбирайте метрику на частини й лагодьте найдовшу, а головний довгостроковий важіль — менше JavaScript на клієнті.
<head> <!-- Заздалегідь відкрити з'єднання з CDN картинок: DNS, TCP, TLS --> <link rel="preconnect" href="https://img.example.com"> <!-- Шрифт потрібен одразу, а в CSS браузер знайде його пізно --> <link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin> <style> @font-face { font-family: Inter; src: url(/fonts/inter.woff2) format("woff2"); font-display: swap; /* текст одразу, резервним шрифтом */ } </style></head><body> <!-- LCP-картинка: високий пріоритет, без lazy, розміри проти CLS --> <img src="https://img.example.com/hero-800.avif" srcset="https://img.example.com/hero-800.avif 800w, https://img.example.com/hero-1600.avif 1600w" sizes="100vw" width="1600" height="900" fetchpriority="high" alt="Autumn sale">
<!-- Нижче першого екрана: завантажиться, коли наблизиться до області перегляду --> <img src="/promo.webp" width="600" height="400" loading="lazy" alt="Gift cards"></body>import { onLCP, onINP, onCLS } from 'web-vitals';
// RUM: кожне значення йде у вашу аналітикуfunction send(metric) { const body = JSON.stringify({ name: metric.name, value: metric.value, rating: metric.rating }); navigator.sendBeacon('/rum', body);}onLCP(send);onINP(send);onCLS(send);
// Код-сплітинг: модуль графіків завантажиться лише після клікуdocument.querySelector('#report').addEventListener('click', async () => { const { renderChart } = await import('./chart.js'); renderChart();});- Чому INP замінив FID? — FID міряв лише затримку до початку обробки першого введення, INP — повну затримку до кадру для всіх взаємодій.
- Чим лабораторні дані відрізняються від польових? — Лабораторія відтворювана й годиться для налагодження, поле показує реальні пристрої й мережі.
- Чим
preloadвідрізняється відprefetch? —preload— ресурс поточної сторінки, вантажиться одразу;prefetch— для наступної навігації, у простої. - Як зменшити CLS від вебшрифтів? —
font-display: optionalабо підгонка метрик резервного шрифту черезsize-adjust. - Чи можна вбудувати критичний CSS за суворої CSP? — Так, через nonce або hash у
style-src; див. Основи вебзахисту.