Перейти к содержанию
Фронтенд

32 вопроса по теме «CSS и рендеринг браузера» на собеседовании

В этом материале — 32 вопроса из русской колоды RecallDeck по теме «CSS и рендеринг браузера». Сначала сформулируйте короткий ответ сами, затем откройте подробный разбор и проверьте примеры, ограничения и отказные случаи.

27 мин чтения32 подробных ответаПроверено 24 августа 2026
Главная мысль

Связывайте ответ с браузером, доступностью, производительностью и пользовательским состоянием — именно там видна зрелость frontend-инженера.

Вопросы и ответы

32 подробных ответа

01

Что такое box model и чем отличается content-box от border-box?

Короткий ответ: Каждый элемент — это прямоугольник из четырёх слоёв: content (содержимое), padding (внутренний отступ), border (рамка), margin (внешний отступ). box-sizing определяет, входят ли padding и border в заданную width/height.

Подробно: Слои изнутри наружу: content → padding → border → margin.

  • content-box (значение по умолчанию): width задаёт ширину только содержимого. Реальная занимаемая ширина = width + padding + border.
  • border-box: width включает в себя padding и border. Реальная ширина = заданному width. Так считать интуитивнее.
.a { box-sizing: content-box; width: 200px; padding: 20px; border: 5px solid; }
/* реальная ширина = 200 + 20*2 + 5*2 = 250px */

.b { box-sizing: border-box; width: 200px; padding: 20px; border: 5px solid; }
/* реальная ширина = 200px (content сжимается до 150px) */

Почти во всех проектах ставят глобально:

*, *::before, *::after { box-sizing: border-box; }

⚠️ Ловушка: margin НЕ входит в border-box — он всегда снаружи и добавляется к занимаемому месту. Также проценты для padding/margin всегда считаются от ширины родителя (даже вертикальные!) — это основа трюка с padding-top: 56.25% для сохранения соотношения сторон 16:9.

02

В чём разница между block, inline, inline-block, none, flex, grid?

Короткий ответ: display управляет тем, как элемент участвует в потоке и какой layout-контекст создаёт для детей.

Подробно:

  • block — занимает всю доступную ширину, начинается с новой строки. Принимает width/height, вертикальные margin/padding. (div, p, section).
  • inline — в строке, ширина по содержимому. Игнорирует width/height и вертикальные margin; горизонтальные padding/margin применяются, но не «раздвигают» строки корректно. (span, a, em).
  • inline-block — в строке (как inline), но принимает width/height и все отступы (как block). Хорош для «кнопок в ряд».
  • none — элемент удаляется из потока и не рендерится (в отличие от visibility: hidden, которое скрывает, но место сохраняет).
  • flex — одномерный layout-контекст (строка ИЛИ колонка) для детей.
  • grid — двумерный layout-контекст (строки И колонки одновременно).
.inline-block-row span { display: inline-block; width: 100px; height: 40px; }

⚠️ Ловушка: у inline-block появляется «фантомный» пробел между элементами из-за переносов строк в HTML (whitespace интерпретируется как символ пробела). Лечится font-size: 0 у родителя, отрицательным margin или переходом на flex.

03

Чем отличаются static, relative, absolute, fixed, sticky? Относительно чего позиционируется элемент?

Короткий ответ: position определяет точку отсчёта для top/right/bottom/left и участие в потоке.

Подробно:

  • static (по умолчанию) — в обычном потоке, смещения top/left игнорируются.
  • relative — остаётся в потоке (место занимает на старом месте), но визуально смещается относительно самого себя. Создаёт точку отсчёта для absolute-детей.
  • absoluteвыпадает из потока, позиционируется относительно ближайшего предка с position != static (а если такого нет — относительно <html>/viewport).
  • fixed — выпадает из потока, позиционируется относительно viewport, не двигается при скролле.
  • sticky — гибрид: ведёт себя как relative, пока скролл не достигнет порога (top: 0), затем «прилипает» как fixed в пределах родителя.
.parent { position: relative; }     /* точка отсчёта */
.child  { position: absolute; top: 0; right: 0; }  /* в правый верхний угол родителя */

.header { position: sticky; top: 0; }  /* прилипает к верху при скролле */

⚠️ Ловушка: position: sticky не работает, если у родителя overflow: hidden/auto/scroll, либо у родителя нет высоты, либо не задан хотя бы один порог (top/bottom). fixed/absolute ломают свою привязку к viewport, если у предка задан transform, filter или will-change — такой предок становится containing block.

04

Объясни Flexbox: главная/поперечная оси, justify-content, align-items, flex-grow/shrink/basis.

Короткий ответ: Flexbox — одномерная раскладка. justify-content выравнивает по главной оси, align-items — по поперечной. flex (grow shrink basis) управляет тем, как элемент растёт/сжимается.

Подробно: Главная ось задаётся flex-direction (row — горизонтальная, column — вертикальная). Поперечная — перпендикулярна ей.

.container {
  display: flex;
  flex-direction: row;          /* главная ось — горизонталь */
  justify-content: space-between;  /* распределение по главной оси */
  align-items: center;          /* выравнивание по поперечной оси */
  gap: 16px;                    /* промежутки */
}
  • justify-content: flex-start | center | flex-end | space-between | space-around | space-evenly.
  • align-items: stretch (default) | center | flex-start | flex-end | baseline.
  • flex-grow — коэффициент роста при наличии свободного места (0 = не растёт).
  • flex-shrink — коэффициент сжатия при нехватке места (1 = может сжиматься).
  • flex-basis — базовый размер до распределения (auto = по содержимому).
.item { flex: 1 1 0; }   /* grow shrink basis — все элементы равной ширины */
.item { flex: 0 0 200px; } /* фиксированные 200px, не растёт, не сжимается */

Когда использовать: один ряд/колонка элементов — навбары, тулбары, выравнивание по центру, распределение кнопок.

⚠️ Ловушка: flex: 1 раскрывается в 1 1 0%, а не 1 1 auto — это разные вещи. С basis: 0 элементы делят пространство поровну, с basis: auto — пропорционально содержимому. Ещё: min-width: auto (по умолчанию) мешает flex-элементу сжаться меньше содержимого — лечится min-width: 0.

05

Когда использовать Grid вместо Flexbox? Расскажи про template и areas.

Короткий ответ: Grid — двумерная раскладка (ряды И колонки сразу). Используйте, когда нужна сетка/макет страницы; Flex — когда раскладка одномерная.

Подробно:

.grid {
  display: grid;
  grid-template-columns: 200px 1fr 1fr;  /* fr — доля свободного места */
  grid-template-rows: auto 1fr auto;
  gap: 16px;
}

Грид-области (areas) дают наглядный макет:

.layout {
  display: grid;
  grid-template-columns: 200px 1fr;
  grid-template-areas:
    "header header"
    "sidebar main"
    "footer footer";
}
.header  { grid-area: header; }
.sidebar { grid-area: sidebar; }
.main    { grid-area: main; }
.footer  { grid-area: footer; }

Полезные функции: repeat(3, 1fr), minmax(200px, 1fr), авто-адаптивная сетка без media queries:

.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); }

⚠️ Ловушка: не противопоставляйте их жёстко — они дополняют друг друга: Grid для общего макета страницы, Flex для содержимого внутри ячеек. fr учитывает gap, а проценты — нет, поэтому 1fr надёжнее 33% для колонок.

06

Как считается специфичность? Что делает !important и как работает каскад?

Короткий ответ: Специфичность — вес селектора в виде кортежа (inline, id, class, tag). При равной специфичности побеждает правило, объявленное позже. !important перебивает обычную специфичность.

Подробно: Считаем по группам (a, b, c, d):

  • a — inline-стиль (style="...") = 1000 условных очков.
  • b — количество id селекторов (#nav) = 100 каждый.
  • c — количество классов, атрибутов, псевдоклассов (.btn, [type], :hover) = 10 каждый.
  • d — количество тегов и псевдоэлементов (div, ::before) = 1 каждый.
#nav .item a       /* 0,1,1,1 = 111 */
.menu .item a      /* 0,0,2,1 = 21  */
ul li a            /* 0,0,0,3 = 3   */
/* первый победит */

* (универсальный) и комбинаторы (>, +, ~) не добавляют специфичности.

Каскад разрешает конфликты в порядке: важность (!important) → специфичность → порядок объявления.

⚠️ Ловушка: !important — последнее средство; войны !important-ов перебиваются только другим !important с большей специфичностью. Inline-стиль с !important практически непобедим (кроме !important в user-stylesheet). Современная альтернатива — @layer (каскадные слои) для управления приоритетом без специфичности.

07

Какие бывают селекторы и комбинаторы? Чем псевдокласс отличается от псевдоэлемента?

Короткий ответ: Комбинаторы: A B (потомок), A > B (прямой ребёнок), A + B (следующий соседний), A ~ B (все последующие соседи). Псевдокласс — состояние (:hover), псевдоэлемент — виртуальная часть (::before).

Подробно:

.list li         { }  /* потомок (любой уровень вложенности) */
.list > li       { }  /* только прямые дети */
h2 + p           { }  /* первый <p> сразу после <h2> */
h2 ~ p           { }  /* все <p> после <h2> на том же уровне */

Псевдоклассы (одно двоеточие) — состояния и позиция:

a:hover { }
input:focus { }
li:nth-child(2n) { }   /* чётные */
li:first-child { }
li:not(.active) { }

Псевдоэлементы (два двоеточия) — генерируемое/частичное содержимое:

.quote::before { content: "«"; }
p::first-line { font-weight: bold; }
input::placeholder { color: gray; }

⚠️ Ловушка: :nth-child(n) считает среди ВСЕХ детей родителя, а :nth-of-type(n) — только среди элементов того же тега. ::before/::after не работают без свойства content (даже content: "") и не появляются у replaced-элементов (img, input).

08

Какие свойства наследуются, а какие нет?

Короткий ответ: Наследуются в основном свойства, связанные с текстом (color, font-*, line-height, text-align, visibility). НЕ наследуются margin, padding, border, width, background, display, position.

Подробно: Можно явно управлять наследованием:

.child { color: inherit; }   /* взять у родителя */
.child { all: unset; }       /* сбросить всё */
button { font: inherit; }    /* кнопки не наследуют шрифт — частый фикс */

Значения inherit (наследовать), initial (значение по спецификации), unset (наследовать если свойство наследуемое, иначе initial), revert (откатить к стилю браузера).

⚠️ Ловушка: form-элементы (button, input, select, textarea) НЕ наследуют font-family/font-size от родителя по умолчанию — поэтому часто пишут button { font: inherit; }. Наследуется вычисленное значение, например line-height: 1.5 наследуется как множитель, а line-height: 24px — как фиксированное значение.

09

Чем отличаются px, em, rem, %, vw, vh, fr? В чём разница em и rem?

Короткий ответ: px — абсолютные пиксели; em — относительно font-size текущего элемента; rem — относительно font-size корня (<html>); % — относительно родителя; vw/vh — % от ширины/высоты viewport; fr — доля свободного места в grid.

Подробно:

html { font-size: 16px; }
.box { font-size: 2rem;  }  /* 32px — от корня */
.box { padding: 1em;     }  /* 32px — от font-size самого .box (=32) */
.full { width: 100vw; height: 100vh; }  /* весь экран */
  • em каскадирует/умножается: вложенные элементы с font-size: 1.5em множат размеры по цепочке → легко получить непредсказуемый результат.
  • rem всегда привязан к одному источнику (корень) → предсказуем, поэтому предпочтителен для типографики и отступов.
  • em удобен, когда нужно масштабировать что-то относительно собственного шрифта (например, padding кнопки в em масштабируется вместе с её текстом).

⚠️ Ловушка: 100vw включает ширину вертикального скроллбара → горизонтальный скролл на десктопе. Новые единицы dvh/svh/lvh (dynamic/small/large viewport height) решают проблему «прыгающей» адресной строки на мобильных, где 100vh некорректен.

10

Что такое stacking context и почему z-index «не работает»?

Короткий ответ: z-index действует только внутри одного контекста наложения. Элемент с высоким z-index внутри контекста с низким z-index всё равно окажется ниже соседнего контекста.

Подробно: Stacking context создаётся, в частности:

  • корневым <html>;
  • элементом с position != static И заданным z-index (не auto);
  • opacity < 1, transform, filter, will-change, mix-blend-mode, isolation: isolate;
  • flex/grid-ребёнком с z-index != auto.
.modal-parent { opacity: 0.99; }     /* создаёт новый контекст! */
.modal { position: fixed; z-index: 9999; }
/* модалка с z-index 9999 окажется ПОД соседним блоком,
   потому что заперта внутри контекста .modal-parent */

Внутри контекста порядок: фон родителя → отрицательные z-index → block-поток → float → inline → z-index:auto/0 → положительные z-index.

⚠️ Ловушка: классическая боль — z-index: 999999 не помогает, потому что родитель с transform или opacity создал изолированный контекст. Решение: вынести элемент выше по дереву (portal) или убрать свойство, создающее контекст. Используйте isolation: isolate, чтобы намеренно создать контекст и не давать z-index «протекать».

11

Что такое margin collapse (схлопывание отступов)?

Короткий ответ: Вертикальные margin соседних блоков и родитель/первый-ребёнок схлопываются в один — берётся наибольший, а не сумма.

Подробно: Три случая:

  1. Соседние блоки: margin-bottom: 20px + margin-top: 30px = промежуток 30px (не 50).
  2. Родитель и первый/последний ребёнок: margin-top ребёнка «вытекает» наружу родителя.
  3. Пустой блок: его собственные top и bottom схлопываются.
.a { margin-bottom: 20px; }
.b { margin-top: 30px; }
/* фактический отступ между .a и .b = 30px */

Как предотвратить схлопывание родителя с ребёнком: добавить родителю padding, border, overflow: hidden, или сделать его flex/grid-контейнером (в них схлопывания нет).

⚠️ Ловушка: схлопываются только вертикальные margin в нормальном потоке. Горизонтальные — никогда. В flex/grid контейнерах схлопывания margin нет вообще — частая причина «почему отступы вдруг изменились» при переводе на flex.

12

Что такое float и зачем нужен clearfix?

Короткий ответ: float исторически использовался для обтекания текстом картинок и для колоночных макетов. clearfix заставляет родителя «увидеть» высоту своих float-детей, которые выпадают из потока.

Подробно:

img { float: left; margin-right: 1em; }  /* текст обтекает картинку */

Проблема: float-элементы не занимают высоту в родителе → родитель «схлопывается» в нулевую высоту. Решение — clearfix:

.clearfix::after {
  content: "";
  display: block;
  clear: both;
}

Современная замена обоих кейсов макета — Flexbox/Grid. float остался только для обтекания текстом.

⚠️ Ловушка: clear: both сбрасывает обтекание И слева, и справа. Сегодня для макетов float не используют — но на собеседовании про clearfix спрашивают как про «легаси, которое надо понимать». Альтернатива clearfix без псевдоэлемента — overflow: auto/display: flow-root у родителя.

13

Как делать адаптивность? Что такое mobile-first и чем responsive отличается от adaptive?

Короткий ответ: Media queries меняют стили по ширине экрана. Mobile-first — пишем базовые стили под мобильный, расширяем через min-width. Responsive — плавная резина; adaptive — несколько фиксированных макетов под брейкпоинты.

Подробно:

/* mobile-first: база — мобильный, расширяем вверх */
.col { width: 100%; }
@media (min-width: 768px)  { .col { width: 50%; } }   /* планшет+ */
@media (min-width: 1024px) { .col { width: 33.33%; } } /* десктоп+ */
  • Responsive — единый макет, плавно тянется (%, fr, clamp(), minmax).
  • Adaptive — несколько заранее свёрстанных вариантов, переключаемых на брейкпоинтах.

Полезно: clamp(min, preferred, max) для «текучей» типографики без media queries:

h1 { font-size: clamp(1.5rem, 4vw, 3rem); }

Не забыть viewport-метатег:

<meta name="viewport" content="width=device-width, initial-scale=1">

⚠️ Ловушка: mobile-first использует min-width (стили добавляются вверх), desktop-first — max-width. Смешивать опасно: перекрытия дают неожиданные результаты. Container queries (@container) — новый подход: стиль зависит от ширины контейнера, а не viewport.

14

Что такое CSS-переменные (custom properties)?

Короткий ответ: Это пользовательские свойства --name, читаемые через var(). В отличие от переменных препроцессоров, они живут в рантайме, каскадируют и наследуются.

Подробно:

:root {
  --primary: #3b82f6;
  --gap: 16px;
}
.btn { background: var(--primary); padding: var(--gap); }
.btn-danger { --primary: #ef4444; }  /* переопределение в области видимости */

Можно менять из JS — отлично для тем:

document.documentElement.style.setProperty('--primary', '#10b981');

var() поддерживает фолбэк: var(--x, 8px).

⚠️ Ловушка: в отличие от Sass-переменных (компилируются и исчезают), CSS-переменные динамические и каскадируют — это плюс для тем, но var() нельзя использовать в media-query условиях (@media (min-width: var(--bp)) не работает). Имена регистрозависимы: --Primary--primary.

15

transition vs animation/keyframes? Какие свойства анимировать дёшево?

Короткий ответ: transition анимирует изменение между двумя состояниями (требует триггера, например :hover). animation + @keyframes — самостоятельная многошаговая анимация с циклами. Дёшево анимировать transform и opacity; дорого — свойства, триггерящие layout (width, top, margin).

Подробно:

/* transition: A -> B по событию */
.btn { transition: transform 0.2s ease; }
.btn:hover { transform: scale(1.1); }

/* animation: самостоятельная, может зациклиться */
@keyframes pulse {
  0%   { opacity: 1; }
  50%  { opacity: 0.5; }
  100% { opacity: 1; }
}
.dot { animation: pulse 1s infinite; }

Почему transform/opacity дёшевы: их умеет анимировать compositor на GPU — без пересчёта layout и без перерисовки слоёв. width, height, top, left, margin вызывают reflow на каждом кадре → дёргается весь экран.

⚠️ Ловушка: анимация box-shadow, filter, background вызывает repaint каждый кадр — дорого. Сместить элемент лучше через transform: translateX(), а не left. will-change: transform подсказывает браузеру создать слой заранее, но злоупотребление им жрёт память.

16

Как отцентрировать элемент? Назови несколько способов.

Короткий ответ: Flexbox (justify-content + align-items), Grid (place-items: center), position + transform, margin: auto.

Подробно:

/* 1. Flexbox — самый частый */
.parent { display: flex; justify-content: center; align-items: center; }

/* 2. Grid — короче всего */
.parent { display: grid; place-items: center; }

/* 3. absolute + transform — без знания размеров */
.child {
  position: absolute; top: 50%; left: 50%;
  transform: translate(-50%, -50%);
}

/* 4. margin: auto в flex */
.child { margin: auto; }

/* 5. горизонтальное центрирование блока известной ширины */
.child { width: 300px; margin: 0 auto; }

⚠️ Ловушка: margin: 0 auto центрирует только по горизонтали и только блочные элементы с заданной шириной; вертикально так не получится (в обычном потоке). transform: translate(-50%, -50%) может давать размытие текста на нецелых пикселях — иногда добавляют will-change или округляют.

17

Кратко: БЭМ, CSS-in-JS, CSS Modules, Tailwind — в чём суть?

Короткий ответ: Это разные подходы к организации стилей и борьбе с глобальностью CSS.

Подробно:

  • БЭМ (Block-Element-Modifier) — соглашение об именовании классов: .card__title--active. Решает конфликты имён дисциплиной, без инструментов. Плоская специфичность (всё классы).
  • CSS Modulesimport styles from './x.module.css', имена хешируются в уникальные (Card_title_a3f9) → локальная область видимости, нет коллизий. Билд-тайм.
  • CSS-in-JS (styled-components, Emotion) — стили в JS, динамические по пропсам. Гибко, но рантайм-оверхед (часть мигрирует на zero-runtime).
  • Tailwind — utility-first: классы-атомы (flex p-4 text-lg) прямо в разметке. Нет нейминга, маленький итоговый CSS (purge неиспользуемого), но «шумная» разметка.
<!-- БЭМ -->        <div class="card card--featured"><h2 class="card__title">…</h2></div>
<!-- Tailwind -->   <div class="rounded-lg p-4 shadow"><h2 class="text-xl font-bold">…</h2></div>

⚠️ Ловушка: у всех общая цель — изоляция и предсказуемость стилей в большом проекте. Не существует «правильного» — выбор зависит от команды/стека. CSS-in-JS даёт runtime-стоимость; Tailwind требует привыкания и хорош с компонентами, чтобы не дублировать классы.

18

Опиши конвейер рендеринга страницы браузером.

Короткий ответ: HTML → DOM, CSS → CSSOM, их объединение → Render Tree, затем Layout (reflow)PaintComposite.

Подробно:

  1. Парсинг HTML → DOM — токенайзер строит дерево узлов. Встретив <script> без async/defer, парсер останавливается.
  2. Парсинг CSS → CSSOM — дерево стилей. CSS render-blocking: рендер ждёт построения CSSOM.
  3. Render Tree — слияние DOM и CSSOM, только видимые узлы (без display: none, но visibility: hidden остаётся).
  4. Layout (reflow) — вычисление геометрии: координаты и размеры каждого узла. Зависит от viewport.
  5. Paint — заливка пикселей по слоям (текст, цвета, тени, границы) в bitmap.
  6. Composite — сборка слоёв в финальную картинку, часто на GPU (учитывает transform, opacity, z-порядок).
HTML ─► DOM ─┐
             ├─► Render Tree ─► Layout ─► Paint ─► Composite ─► экран
CSS  ─► CSSOM┘

⚠️ Ловушка: display: none исключает узел из render tree (нет layout/paint), а visibility: hidden/opacity: 0 оставляют его в layout (место занято). JS может читать стили посреди этого конвейера и форсировать синхронный reflow.

19

Reflow vs repaint vs composite — что дороже и что их триггерит?

Короткий ответ: Reflow (layout) — пересчёт геометрии, самый дорогой. Repaint — перерисовка пикселей без изменения геометрии, дешевле. Composite — пересборка слоёв на GPU, самый дешёвый.

Подробно:

Этап Что меняется Триггеры Стоимость
Reflow/Layout размеры/позиции width, height, top/left, margin, font-size, добавление DOM-узлов, чтение offsetWidth 🔴 высокая
Repaint цвета/видимое color, background, box-shadow, visibility 🟡 средняя
Composite только слой transform, opacity (на отдельном слое) 🟢 низкая

Reflow тянет за собой repaint и composite (каскад). Поэтому минимизируют именно reflow.

Как минимизировать:

  • Анимировать transform/opacity, а не top/width.
  • Батчить DOM-изменения, избегать layout thrashing (чередования записи и чтения геометрии):
// ПЛОХО: forced synchronous layout в цикле
for (const el of items) {
  el.style.height = el.offsetHeight + 10 + 'px'; // чтение → запись → чтение…
}
// ХОРОШО: сначала все чтения, потом все записи
const hs = items.map(el => el.offsetHeight);
items.forEach((el, i) => el.style.height = hs[i] + 10 + 'px');

⚠️ Ловушка: чтение offsetTop/offsetWidth/getBoundingClientRect()/getComputedStyle() сразу после изменения стиля форсирует синхронный reflow — браузер вынужден немедленно пересчитать layout. Это «layout thrashing» и главный источник тормозов в скриптах с анимацией.

20

Что такое critical rendering path и render-blocking ресурсы? Почему CSS в head, а JS — внизу/async/defer?

Короткий ответ: Critical Rendering Path — последовательность шагов от получения HTML до первого пикселя. CSS блокирует рендеринг, синхронный JS блокирует и парсинг, и рендеринг. Поэтому CSS грузят рано (в <head>), а скрипты — не блокирующе.

Подробно:

  • CSS render-blocking: браузер не отрисует контент, пока не построит CSSOM (иначе был бы FOUC — мелькание нестилизованного контента). Значит, CSS нужен как можно раньше и минимальным (см. Critical CSS).
  • Синхронный <script> parser-blocking: встретив его, парсер останавливает построение DOM, скачивает и выполняет скрипт (а скрипт ещё ждёт CSSOM, если читает стили). Поэтому исторически клали скрипты перед </body>.
<head>
  <link rel="stylesheet" href="styles.css">  <!-- грузим стили рано -->
</head>
<body>
  …контент…
  <script src="app.js" defer></script>        <!-- не блокирует парсинг -->
</body>

Цель — сократить путь: меньше критических ресурсов, меньше их объём, меньше «прыжков» туда-обратно по сети.

⚠️ Ловушка: CSS блокирует не только paint, но и выполнение JS, который идёт после него (браузер боится, что скрипт прочитает ещё не готовые стили). Поэтому огромный CSS тормозит и JS. @import внутри CSS добавляет лишний последовательный запрос — лучше <link>.

21

В чём разница между async и defer у `<script>`?

Короткий ответ: Оба грузят скрипт параллельно с парсингом HTML (не блокируют его при загрузке). defer выполняет скрипты после парсинга DOM и в порядке объявления; async выполняет сразу по загрузке, в произвольном порядке, прерывая парсинг.

Подробно:

<script src="a.js"></script>           <!-- блокирует парсинг: download + execute -->
<script src="a.js" async></script>     <!-- download ∥ parse, execute ASAP -->
<script src="a.js" defer></script>     <!-- download ∥ parse, execute после DOMContentLoaded-этапа, по порядку -->
Когда грузится Когда выполняется Порядок
обычный блокирует парсинг сразу по порядку
async параллельно как только скачан (может прервать парсинг) НЕ гарантирован
defer параллельно после полного парсинга DOM по порядку
  • defer — для скриптов, которым нужен готовый DOM и важна последовательность (большинство app-кода).
  • async — для независимых скриптов (аналитика, реклама), которым всё равно когда и в каком порядке.

⚠️ Ловушка: async/defer игнорируются у inline-скриптов (без src). Скрипты типа module ведут себя как defer по умолчанию. async-скрипт может выполниться раньше, чем DOM готов, — нельзя полагаться на наличие элементов.

22

Что такое Core Web Vitals (LCP, INP/FID, CLS) и метрики TTFB, FCP?

Короткий ответ: Core Web Vitals — три ключевые метрики Google: LCP (скорость загрузки), INP/FID (отзывчивость), CLS (визуальная стабильность). TTFB и FCP — вспомогательные метрики времени.

Подробно:

  • TTFB (Time To First Byte) — время до первого байта ответа сервера. Влияет на всё последующее.
  • FCP (First Contentful Paint) — первый отрисованный контент (текст/изображение).
  • LCP (Largest Contentful Paint) — отрисовка крупнейшего видимого элемента (главный hero/картинка). Цель: < 2.5 с.
  • FID (First Input Delay) — задержка реакции на первый ввод. Заменён на INP (Interaction to Next Paint) — отзывчивость на протяжении всей сессии. Цель INP: < 200 мс.
  • CLS (Cumulative Layout Shift) — суммарный сдвиг макета (когда контент «прыгает»). Цель: < 0.1.

Как улучшать:

  • LCP — preload hero-изображения/шрифта, быстрый сервер/CDN, оптимизация критического CSS.
  • INP — дробить долгие JS-задачи, requestIdleCallback, web workers.
  • CLS — задавать width/height у img, резервировать место под рекламу/баннеры, font-display: optional.
<img src="hero.jpg" width="1200" height="600" alt="…">  <!-- резерв места против CLS -->
<link rel="preload" as="image" href="hero.jpg">          <!-- ускоряет LCP -->

⚠️ Ловушка: CLS чаще всего ломают изображения без width/height, динамически вставляемые баннеры и шрифты, меняющие метрики при подгрузке. FID измеряет только первое взаимодействие — INP честнее, потому что учитывает всю сессию.

23

Какие есть способы оптимизации загрузки страницы?

Короткий ответ: Уменьшать объём (минификация, сжатие, code splitting), грузить отложенно (lazy loading, defer), кэшировать (HTTP cache, CDN), предзагружать критичное (preload/prefetch).

Подробно:

  • Минификация — удаление пробелов/комментариев из JS/CSS/HTML.
  • Сжатие на транспорте — gzip/brotli (br эффективнее).
  • Lazy loading изображений и iframe: <img loading="lazy">; для компонентов — динамический импорт.
  • Code splitting — разбить бандл по роутам/фичам, грузить по требованию (import()), tree-shaking убирает мёртвый код.
  • КэшированиеCache-Control, хешированные имена файлов (app.a3f9.js) для долгого кэша + мгновенной инвалидации.
  • CDN — раздача статики с ближайшего к пользователю узла (меньше latency).
  • Resource hints: preload (нужно сейчас, высокий приоритет), prefetch (понадобится позже), preconnect/dns-prefetch (заранее установить соединение).
<link rel="preload"  as="font" href="font.woff2" crossorigin>  <!-- критичный шрифт -->
<link rel="prefetch" href="/next-page.js">                     <!-- следующий роут -->
<link rel="preconnect" href="https://api.example.com">         <!-- заранее открыть соединение -->
<script type="module" src="main.js"></script>                  <!-- современный бандл -->

Дерево зависимостей бандла — анализатор (webpack-bundle-analyzer) показывает, что раздуло сборку → удаляем тяжёлые/дублирующиеся либы, переходим на легковесные альтернативы.

⚠️ Ловушка: не злоупотреблять preload — слишком много высокоприоритетных запросов конкурируют за пропускную способность и тормозят критичные. lazy нельзя ставить на LCP-изображение (hero) — это замедлит LCP. Tree-shaking не работает на CommonJS-модулях и при сайд-эффектах.

24

Что такое Critical CSS и проблемы шрифтов FOUT/FOIT?

Короткий ответ: Critical CSS — минимальный набор стилей для контента «над сгибом», инлайнится в <head> для быстрого первого рендера. FOUT (Flash of Unstyled Text) — текст системным шрифтом до загрузки кастомного; FOIT (Flash of Invisible Text) — невидимый текст, пока шрифт грузится.

Подробно:

Critical CSS: выделяем стили видимой части, инлайним их, остальной CSS грузим асинхронно — чтобы первый paint не ждал большого таблицы стилей:

<head>
  <style>/* critical: hero, header, шрифты */</style>
  <link rel="preload" href="full.css" as="style" onload="this.rel='stylesheet'">
</head>

Шрифты управляются font-display:

  • swap — сразу показать fallback, потом подменить (FOUT) — текст всегда виден.
  • block — короткая невидимость, ждём шрифт (FOIT).
  • optional — если шрифт не успел, используем системный и не дёргаем layout (лучший для CLS).
@font-face {
  font-family: 'Inter';
  src: url('inter.woff2') format('woff2');
  font-display: swap;
}

⚠️ Ловушка: FOUT даёт сдвиг макета (CLS), если у кастомного и fallback-шрифта разные метрики — лечится size-adjust/ascent-override или font-display: optional. Используйте woff2 (лучшее сжатие), подгружайте только нужные начертания, preload для критичных шрифтов.

25

Что такое DOM? Почему операции с ним дорогие и что такое DocumentFragment?

Короткий ответ: DOM (Document Object Model) — древовидное объектное представление HTML, через которое JS читает и меняет страницу. Изменения DOM дороги, потому что могут триггерить reflow/repaint. DocumentFragment — лёгкий offscreen-контейнер для батч-вставки.

Подробно: Каждое изменение DOM может заставить браузер пересчитать layout и перерисовать. Вставка узлов по одному в цикле = много reflow.

// ПЛОХО: 1000 вставок в живой DOM → потенциально 1000 reflow
for (let i = 0; i < 1000; i++) {
  list.appendChild(document.createElement('li'));
}

// ХОРОШО: собираем во fragment, вставляем разом → 1 reflow
const frag = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
  frag.appendChild(document.createElement('li'));
}
list.appendChild(frag);

Дорогие операции: чтение геометрии (offsetWidth, getBoundingClientRect), частые вставки/удаления, изменения, влияющие на размеры.

⚠️ Ловушка: innerHTML += в цикле — антипаттерн: каждый раз перепарсивается и пересобирается весь HTML, теряются обработчики событий. Лучше собрать строку и присвоить один раз, либо использовать fragment. Виртуальный DOM (React) существует именно чтобы минимизировать реальные DOM-операции через диффинг и батчинг.

26

Объясни event delegation, bubbling/capturing, preventDefault/stopPropagation.

Короткий ответ: События проходят три фазы: capture (сверху вниз), target, bubbling (снизу вверх). Делегирование — один обработчик на родителе ловит события всплывающих детей. preventDefault() отменяет действие браузера по умолчанию; stopPropagation() останавливает дальнейшее распространение.

Подробно:

// Делегирование: один слушатель вместо сотни
document.querySelector('#list').addEventListener('click', (e) => {
  const li = e.target.closest('li');
  if (li) handle(li.dataset.id);
});
  • Capturing — событие идёт от document вниз к target (addEventListener(type, fn, true)).
  • Target — достигает целевого элемента.
  • Bubbling — всплывает обратно вверх (по умолчанию слушатели срабатывают здесь).
e.preventDefault();        // отменить дефолт (переход по ссылке, отправку формы)
e.stopPropagation();       // не пускать событие дальше по дереву
e.stopImmediatePropagation(); // + не вызывать другие слушатели на этом же элементе

⚠️ Ловушка: preventDefault() и stopPropagation() — разные вещи: первый не останавливает всплытие, второй не отменяет дефолтное действие. Не все события всплывают (focus, blur, mouseenter — не всплывают; есть всплывающие аналоги focusin, mouseover). Делегирование не сработает для неподнимающихся событий.

27

Чем отличаются localStorage, sessionStorage и cookies?

Короткий ответ: Все хранят данные на клиенте. localStorage — постоянно, sessionStorage — на время вкладки, cookies — отправляются на сервер с каждым запросом и имеют срок жизни.

Подробно:

localStorage sessionStorage cookies
Срок жизни пока не удалят до закрытия вкладки по expires/max-age
Объём ~5–10 МБ ~5 МБ ~4 КБ
Отправка на сервер нет нет да, с каждым запросом
Область origin, общий для вкладок только текущая вкладка origin + path
API синхронный JS синхронный JS document.cookie / заголовки
localStorage.setItem('theme', 'dark');     // переживёт перезагрузку
sessionStorage.setItem('step', '2');       // только эта вкладка/сессия
document.cookie = 'token=abc; Secure; SameSite=Strict; HttpOnly';

Когда что:

  • localStorage — настройки UI, тема, кэш некритичных данных.
  • sessionStorage — данные одного сеанса/мастера, не нужные после закрытия.
  • cookies — аутентификация/сессии, что нужно серверу (особенно HttpOnly для токенов — недоступны JS, защита от XSS).

⚠️ Ловушка: localStorage/sessionStorage синхронны и блокируют поток — не складывать туда мегабайты. Они хранят только строки (объекты — через JSON.stringify). Токены в localStorage уязвимы к XSS; для сессий безопаснее HttpOnly+Secure cookies. Cookies шлются с каждым запросом → раздувают трафик.

28

Что важно знать про доступность (a11y)?

Короткий ответ: Семантический HTML, текстовые альтернативы (alt), ARIA-атрибуты там, где семантики не хватает, и достаточный цветовой контраст.

Подробно:

  • Семантика: <button>, <nav>, <header>, <main>, <h1>…<h6> дают структуру скринридерам и клавиатурную навигацию бесплатно. Не делать кнопку из <div onclick>.
  • alt у изображений: описание для незрячих; пустой alt="" для декоративных.
  • ARIA: aria-label, role, aria-expanded, aria-live — когда нативной семантики недостаточно (кастомные виджеты).
  • Контраст: текст к фону ≥ 4.5:1 (WCAG AA), крупный — ≥ 3:1.
  • Клавиатура: всё кликабельное должно быть доступно с Tab/Enter; видимый :focus.
<button aria-label="Закрыть" aria-expanded="false">✕</button>
<img src="logo.png" alt="Логотип компании">
<img src="divider.png" alt="">  <!-- декоративное -->

⚠️ Ловушка: «первое правило ARIA — не использовать ARIA, если есть нативный элемент». aria-label на неинтерактивном элементе бесполезен. Убирать outline с :focus без замены — частая ошибка доступности; используйте :focus-visible. Плейсхолдер не заменяет <label>.

29

Что происходит, когда ты вводишь URL и жмёшь Enter? (фокус на рендеринге)

Короткий ответ: DNS-резолв → TCP/TLS-соединение → HTTP-запрос → ответ сервера → браузер парсит HTML в DOM, CSS в CSSOM, строит render tree, делает layout, paint, composite и выводит пиксели.

Подробно:

  1. DNS — доменное имя → IP-адрес (с кэшированием на разных уровнях).
  2. TCP + TLS — установка соединения и шифрования (для HTTPS handshake).
  3. HTTP-запрос → сервер отдаёт HTML (TTFB — время до первого байта).
  4. Парсинг HTML → DOM, попутно браузер «препарсером» находит и предзагружает ресурсы.
  5. CSS → CSSOM (render-blocking), JS выполняется (parser-blocking, если синхронный).
  6. Render Tree = DOM + CSSOM (видимые узлы).
  7. Layout (reflow) — геометрия; Paint — пиксели по слоям; Composite — сборка (часто GPU).
  8. FCP/LCP — пользователь видит контент; затем гидрация/интерактивность.
URL → DNS → TCP/TLS → HTTP → HTML
                                 ├─► DOM ─┐
                                 │        ├─► Render Tree → Layout → Paint → Composite → пиксели
                                 └─► CSS ─► CSSOM┘

⚠️ Ловушка: на собесе по фронту акцент делают именно на части «после получения HTML» (рендеринг), а не на сетевой. Упомяни render-blocking CSS, parser-blocking JS, critical rendering path и почему важны метрики (LCP/FCP).

30

Почему манипуляции с DOM «дорогие»?

Короткий ответ: Изменение DOM может запускать reflow (пересчёт геометрии всей страницы) и repaint, а чтение геометрии сразу после записи форсирует синхронный layout. Эти пересчёты — самая дорогая часть кадра.

Подробно: DOM сам по себе — просто дерево объектов, доступ к нему не медленный. Дорог побочный эффект: браузеру приходится заново считать layout и перерисовывать. Чем больше дерево и чем чаще чередуются чтения/записи, тем хуже.

// layout thrashing: запись → чтение → запись → чтение… каждый шаг = reflow
box.style.width = '100px';
console.log(box.offsetHeight);  // форсирует reflow
box.style.height = '50px';
console.log(box.offsetTop);     // ещё reflow

Способы удешевить: батчить изменения, использовать DocumentFragment, менять класс вместо множества инлайн-стилей, читать геометрию пачкой до записей, анимировать transform/opacity, применять requestAnimationFrame для синхронизации с кадром.

⚠️ Ловушка: именно из-за этого появились виртуальные DOM (React) и батчинг — они собирают изменения и применяют минимальный набор реальных операций за один проход, избегая лишних reflow.

31

Почему анимировать transform дешевле, чем top/left?

Короткий ответ: transform/opacity обрабатываются на этапе composite (часто на GPU) без пересчёта layout и перерисовки. top/left/width/margin меняют геометрию → запускают reflow + repaint на каждом кадре.

Подробно: Анимация идёт 60 раз в секунду. Если каждый кадр требует полного reflow всей страницы, браузер не успевает за 16.6 мс и кадры «дропаются» (дёргание).

/* ДОРОГО: каждый кадр = layout + paint + composite */
.bad  { transition: left 0.3s; }
.bad:hover { left: 100px; }

/* ДЁШЕВО: только composite на GPU */
.good { transition: transform 0.3s; }
.good:hover { transform: translateX(100px); }

Браузер может вынести анимируемый по transform/opacity элемент на отдельный композитный слой и двигать его независимо от остального layout. will-change: transform подсказывает создать слой заранее.

⚠️ Ловушка: дешевизна работает, только пока элемент на собственном слое; чрезмерное число слоёв или will-change на всём подряд раздувает память GPU. transform влияет на визуал, но не на занимаемое в потоке место — другие элементы не сдвинутся.

32

Зачем нужен event delegation?

Короткий ответ: Чтобы вместо сотен обработчиков на каждом дочернем элементе повесить один на общего родителя, используя всплытие. Это экономит память и автоматически работает для динамически добавленных элементов.

Подробно:

// Без делегирования: N слушателей, и новые элементы их не получат
document.querySelectorAll('.item').forEach(el =>
  el.addEventListener('click', handler)
);

// С делегированием: 1 слушатель, работает и для будущих .item
document.querySelector('#list').addEventListener('click', (e) => {
  const item = e.target.closest('.item');
  if (item) handler(item);
});

Плюсы:

  • Меньше слушателей → меньше памяти и быстрее навешивание.
  • Динамически добавленные элементы обрабатываются без перенавешивания.
  • Проще управлять (один источник истины).

⚠️ Ловушка: работает только для всплывающих событий; для focus/blur/mouseenter нужны невсплывающие исключения (берём focusin/mouseover). Обязательно проверять e.target.closest(...), иначе клик по вложенным элементам внутри .item может промахнуться. Нужно учитывать stopPropagation() внутри детей — он сломает делегирование.

Источники

Источники и редакционная политика

Материалы RecallDeck сопоставлены с официальной документацией и открытыми публикациями компаний, когда первичный источник доступен. Мы не связаны с упомянутыми работодателями, не публикуем конфиденциальные задания и не продаём места в подборках. Формат найма может меняться — уточняйте его у рекрутера.

От чтения к воспроизведению

Отрепетируйте полный цикл интервью.

RecallDeck возвращает сложные темы по расписанию и помогает удерживать в памяти язык, SQL, архитектуру и поведенческие истории.

Начать подготовку

Продолжить подготовку

Библиотека собеседований RecallDeck

Подробные русские ответы, разборы этапов найма и планы подготовки для российского IT-рынка.

RSS