Перейти до вмісту

Вебплатформа

Що браузер робить зі сторінкою незалежно від фреймворку: як розкладає блоки, як робить інтерфейс доступним, як перетворює HTML на пікселі і як це пришвидшити. Angular і React працюють поверх цього шару, тому на fullstack-співбесіді його перевіряють окремо.

Перед цією темою: JavaScript і TypeScript.

Junior+

Як влаштована верстка в CSS: блокова модель, flexbox і grid?

Section titled “Як влаштована верстка в CSS: блокова модель, flexbox і grid?”
Відповідь

Що це

  • Блокова модель. Елемент — це content, padding, border і margin. За замовчуванням width не включає padding і border.
  • display. block займає весь рядок, inline тече в тексті, flex і grid розкладають дітей елемента.
  • Flexbox ставить дітей у рядок або колонку, grid — у сітку рядків і колонок.

Як користуватися

  1. Для всіх елементів — box-sizing: border-box.
  2. Каркас сторінки — grid, елементи всередині блока — flex, відстані — gap.
  3. Стилі за замовчуванням — для телефона, для широких екранів — @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); }
Middle+

Як зробити інтерфейс доступним?

Section titled “Як зробити інтерфейс доступним?”
Відповідь

Що це

Доступність — інтерфейсом можуть користуватися люди з порушеннями зору, слуху й моторики: зі скринрідером, лише з клавіатури, зі збільшеним шрифтом.

Як користуватися

  • Дія — <button>, перехід — <a href>, а не div з обробником кліку: вони фокусуються й натискаються з клавіатури.
  • У кожного поля — <label for="...">. Плейсхолдер підписом не вважається: він зникає під час введення.
  • У картинки — alt з її змістом, у декоративної — порожній alt="".
  • Заголовки h1–h6 ідуть по порядку, області розмічені header, nav, main.
  • Контраст звичайного тексту з тлом — не менше 4,5:1.

Типова помилка

Прибрати рамку фокуса через outline: none, бо «некрасиво». Користувач клавіатури перестає бачити, де він. Рамку не прибирають, а оформлюють через :focus-visible.

Як влаштовано

  1. З DOM і CSS браузер будує дерево доступності: роль (button), доступне ім’я («Зберегти») і стан (disabled, expanded).
  2. Скринрідер — NVDA, JAWS, VoiceOver, TalkBack — отримує це дерево через API доступності ОС і озвучує.
  3. 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;
}
Middle+

Як браузер перетворює HTML на пікселі?

Section titled “Як браузер перетворює HTML на пікселі?”
Відповідь

Що це

Шлях від HTML до пікселів:

  1. DOM. HTML розбирається в дерево вузлів.
  2. CSSOM. CSS розбирається в дерево правил.
  3. Дерево відмальовування (render tree). Лише видимі елементи з підсумковими стилями: display: none сюди не потрапляє.
  4. Layout. Розрахунок розмірів і координат кожного блока.
  5. Paint. Заливання пікселів: текст, кольори, тіні, картинки.
  6. 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>
Layout thrashing і анімація
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; }
Senior+

Як виміряти й пришвидшити завантаження вебзастосунку?

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();
});