Перейти к содержимому

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

Что браузер делает со страницей независимо от фреймворка: как раскладывает блоки, как делает интерфейс доступным, как превращает HTML в пиксели и как это ускорить. Angular и React работают поверх этого слоя, поэтому на fullstack-собеседовании его проверяют отдельно.

Перед этой темой: JavaScript и TypeScript.

Junior+

Как устроена вёрстка в CSS: блочная модель, flexbox и grid?

Заголовок раздела «Как устроена вёрстка в 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+
Ответ

Что это

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

Как пользоваться

  • Действие — <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 к пикселям:

  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+

Как измерить и ускорить загрузку веб-приложения?

Заголовок раздела «Как измерить и ускорить загрузку веб-приложения?»
Ответ

Что это

Метрика Что измеряет Хорошо
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();
});