Связывайте ответ с браузером, доступностью, производительностью и пользовательским состоянием — именно там видна зрелость frontend-инженера.
Вопросы и ответы
10 подробных ответов
01Что такое Critical Rendering Path и какие ресурсы его блокируют?
middle
Короткий ответ: Critical Rendering Path (критический путь рендеринга) — это последовательность шагов браузера от получения HTML до первого пикселя на экране: DOM + CSSOM → render tree → layout → paint → composite. Синхронный CSS и блокирующий JS задерживают этот путь.
Подробно:
- DOM — браузер парсит HTML в дерево узлов.
- CSSOM — параллельно строится объектная модель стилей; CSS блокирует рендеринг, так как без него нельзя посчитать стили.
- Render tree — DOM и CSSOM объединяются, узлы с
display: noneотбрасываются. - Layout (reflow) — вычисляются геометрия и позиции элементов.
- Paint — узлы растеризуются в слои пикселей.
- Composite — слои собираются на экране (часто на GPU).
HTML ─► DOM ─┐
├─► Render Tree ─► Layout ─► Paint ─► Composite ─► экран
CSS ─► CSSOM┘
▲ блокирует рендеринг ▲ <script> без defer/async
⚠️ Частая ошибка: считать, что JS никогда не мешает рендерингу. Синхронный <script> в <head> останавливает парсинг HTML и построение DOM — используйте defer или вынесите скрипты вниз.
02В чём разница между reflow, repaint и composite, и какие CSS-свойства дёшево анимировать?
middle
Короткий ответ: Reflow (layout) пересчитывает геометрию, repaint перерисовывает пиксели без изменения геометрии, composite лишь пересобирает уже отрисованные слои. Дёшево анимировать только transform и opacity — они идут на этап composite, минуя layout и paint.
Подробно:
- Reflow — самый дорогой: меняет размеры/позиции (
width,top,margin, добавление узлов) и каскадно затрагивает потомков и соседей. - Repaint — перерисовка без сдвига геометрии (
color,background,visibility). - Composite — GPU объединяет готовые слои; layout и paint не запускаются.
| Свойство | Этап | Стоимость |
|---|---|---|
width, top, margin |
layout → paint → composite | высокая |
color, background |
paint → composite | средняя |
transform, opacity |
composite | низкая |
⚠️ Частая ошибка: анимировать left/top или width/height вместо transform: translate() / scale() — это вызывает reflow на каждый кадр и роняет FPS.
03Что такое Core Web Vitals (LCP, INP, CLS), какие у них пороги и как их улучшать?
senior
Короткий ответ: Core Web Vitals — три ключевые метрики Google: LCP (загрузка), INP (отзывчивость) и CLS (визуальная стабильность). С марта 2024 INP официально заменил FID как метрику отзывчивости.
Подробно:
- LCP (Largest Contentful Paint) — время отрисовки самого крупного элемента. Улучшается приоритизацией LCP-изображения (
fetchpriority="high",preload), быстрым TTFB, отказом от блокирующих ресурсов. - INP (Interaction to Next Paint) — задержка от взаимодействия до следующего кадра по всем интеракциям за сессию. Улучшается разбиением длинных задач,
requestIdleCallback, меньшим объёмом JS. - CLS (Cumulative Layout Shift) — суммарный сдвиг макета. Улучшается резервированием места под изображения (
width/height),font-display, отсутствием вставок над контентом.
| Метрика | Что измеряет | Хорошо | Плохо |
|---|---|---|---|
| LCP | загрузка | ≤ 2.5 с | > 4.0 с |
| INP | отзывчивость | ≤ 200 мс | > 500 мс |
| CLS | стабильность | ≤ 0.1 | > 0.25 |
⚠️ Частая ошибка: называть FID действующей метрикой. FID убрали в марте 2024 — отзывчивость теперь измеряет INP, который учитывает всю интеракцию, а не только первую задержку.
04Чем отличаются defer и async у скриптов, и зачем нужны preload, prefetch и preconnect?
middle
Короткий ответ: async грузит скрипт параллельно и выполняет сразу по готовности (порядок не гарантирован); defer грузит параллельно, но выполняет по порядку после парсинга DOM. preload — приоритетная загрузка ресурса для текущей страницы, prefetch — фоновая для будущих, preconnect — заранее открыть соединение.
Подробно:
- defer — для скриптов, зависящих от DOM и друг от друга; сохраняет порядок, не блокирует парсинг.
- async — для независимых скриптов (аналитика); выполняется как только загрузился.
- preload — «нужно сейчас»: шрифт, LCP-картинка, критичный CSS.
- prefetch — «понадобится потом»: ресурсы следующей навигации, с низким приоритетом.
- preconnect — заранее выполнить DNS + TLS-handshake к стороннему домену (CDN, шрифты).
<script src="app.js" defer></script>
<script src="analytics.js" async></script>
<link rel="preload" href="hero.avif" as="image" fetchpriority="high">
<link rel="prefetch" href="/next-page.js">
<link rel="preconnect" href="https://cdn.example.com">
<img src="below-fold.jpg" loading="lazy" alt="...">
⚠️ Частая ошибка: ставить async скриптам, которые зависят от порядка загрузки — выполнение «кто первый» ломает зависимости. Для таких случаев нужен defer.
05Как уменьшить размер бандла: code splitting, dynamic import(), tree shaking?
senior
Короткий ответ: Грузите меньше JS наперёд: разбивайте бандл по маршрутам/компонентам через динамический import() (code splitting), удаляйте мёртвый код через tree shaking (требует ESM-модулей), и анализируйте состав бандла визуализатором.
Подробно:
- Code splitting — делите код на чанки, грузимые по требованию; снижает первоначальную загрузку.
- Dynamic import() — ленивая подгрузка модуля/компонента в момент необходимости (роут, модалка).
- Tree shaking — сборщик отбрасывает неиспользуемые экспорты. Работает только с
import/export(ESM), не с CommonJSrequire, и ломается о побочные эффекты — помогает"sideEffects": false. - Анализ —
rollup-plugin-visualizer/webpack-bundle-analyzerпоказывают, что раздувает бандл (часто тяжёлые библиотеки дат/иконок).
// Статический импорт — попадает в основной бандл
import heavyChart from "chart-lib";
// Динамический — выделяется в отдельный чанк, грузится по требованию
const Chart = React.lazy(() => import("./Chart"));
button.addEventListener("click", async () => {
const { renderModal } = await import("./modal.js");
renderModal();
});
⚠️ Частая ошибка: надеяться на tree shaking при импорте всей библиотеки (import _ from "lodash") или из CommonJS-сборки — импортируйте точечно (import debounce from "lodash/debounce").
06Как работает кэширование в браузере: Cache-Control, ETag, хешированные имена файлов и Service Worker?
middle
Короткий ответ: HTTP-кэш управляется заголовками Cache-Control (на сколько и как кэшировать) и валидаторами ETag/Last-Modified (проверка свежести через 304). Хеш в имени файла (app.a1b2c3.js) обеспечивает cache busting, а Service Worker даёт программный кэш для офлайна.
Подробно:
- Cache-Control —
max-ageзадаёт TTL;immutableотключает повторную валидацию;no-cacheозначает «кэшируй, но проверяй». - ETag / Last-Modified — при истечении TTL браузер шлёт
If-None-Match; сервер отвечает304 Not Modifiedбез тела, экономя трафик. - Хешированные имена — меняется контент → меняется имя → старый кэш игнорируется. Поэтому ассеты раздают с
max-age=31536000, immutable, а HTML —no-cache. - Service Worker — перехватывает запросы (
fetch), реализует стратегии (cache-first, stale-while-revalidate), работает офлайн.
Cache-Control: public, max-age=31536000, immutable # app.a1b2c3.js
Cache-Control: no-cache # index.html
ETag: "5d8c72a..."
⚠️ Частая ошибка: ставить долгий max-age на HTML без хеша — пользователи застревают на старой версии. Хешируйте ассеты, а entry-point HTML держите на no-cache.
07Как эффективно рендерить большие списки и что такое виртуализация (windowing)?
senior
Короткий ответ: Виртуализация (windowing) рендерит только видимые в окне просмотра строки плюс небольшой буфер, а не все тысячи DOM-узлов. Это держит число узлов и время layout/paint константными независимо от длины списка.
Подробно:
- Проблема — 10 000 строк = 10 000 DOM-узлов: огромный layout, медленный paint, раздутая память, лаги при скролле.
- Идея — контейнер задаёт полную высоту (
itemCount × itemHeight), но в DOM существуют только видимые строки, спозиционированные черезtransform: translateY. - Окно (window) — при скролле пересчитывается видимый диапазон индексов; за пределами viewport рендерится небольшой overscan-буфер, чтобы скролл был плавным.
- Инструменты —
react-window/@tanstack/virtualдля React; для динамической высоты строк нужна оценка и измерение.
Реальный список Виртуализированный DOM
┌───────────────┐ ┌───────────────┐ ← scroll container
│ row 0 │ │ row 142 │ (только видимое
│ ... │ │ row 143 │ окно + overscan)
│ row 9999 │ │ row 144 │
└───────────────┘ └───────────────┘
10000 узлов ~12 узлов в DOM
⚠️ Частая ошибка: пытаться «починить» лаги большого списка через React.memo и пагинацию, оставив все узлы в DOM. Узкое место — размер DOM и layout; решает именно виртуализация.
08Как оптимизировать изображения и шрифты: WebP/AVIF, srcset/sizes и font-display?
middle
Короткий ответ: Используйте современные форматы (AVIF/WebP вместо JPEG/PNG), отдавайте под размер экрана через srcset/sizes (responsive images), а для шрифтов задавайте font-display: swap, чтобы избежать FOIT (невидимый текст), и preload для критичных шрифтов.
Подробно:
- Форматы — AVIF сжимает сильнее всего, WebP — широкая поддержка;
<picture>даёт фолбэк на JPEG. - Responsive images —
srcsetперечисляет варианты по ширине,sizesподсказывает браузеру отображаемый размер, чтобы выбрать минимально достаточный файл. - font-display —
swapсразу показывает системный шрифт и подменяет на загруженный (FOUT вместо FOIT);optionalэкономнее на медленных сетях. - Стабильность — задавайте
width/heightилиaspect-ratio, чтобы не было сдвига макета (CLS).
| Формат | Прозрачность | Сжатие | Когда |
|---|---|---|---|
| AVIF | да | лучшее | hero, фото |
| WebP | да | хорошее | дефолт + фолбэк |
| SVG | да | вектор | иконки, лого |
<picture>
<source type="image/avif" srcset="hero-480.avif 480w, hero-960.avif 960w" sizes="(max-width:600px) 480px, 960px">
<img src="hero.jpg" width="960" height="540" alt="...">
</picture>
@font-face { font-family: Inter; src: url(inter.woff2) format("woff2"); font-display: swap; }
⚠️ Частая ошибка: грузить один огромный JPEG для всех экранов. Без srcset мобильные устройства качают десктопную версию, раздувая LCP и трафик.
09Как измерять производительность: Lighthouse, Performance API / web-vitals и разница lab vs field (RUM)?
senior
Короткий ответ: Lighthouse и DevTools дают лабораторные (lab) данные — синтетический прогон в контролируемых условиях. Performance API и библиотека web-vitals собирают полевые (field/RUM) данные с реальных пользователей. Lab нужен для отладки, field — для оценки реального опыта.
Подробно:
- Lab (лабораторные) — Lighthouse, WebPageTest: воспроизводимо, с заданным троттлингом сети/CPU; удобно ловить регрессии в CI, но не отражает разнообразие реальных устройств.
- Field / RUM — данные с настоящих пользователей (CrUX,
web-vitals): отражают реальные сети, девайсы, географию. INP и CLS корректно меряются только в поле, по всей сессии. - Performance API —
PerformanceObserver,performance.getEntriesByType('navigation'),paint— низкоуровневый доступ к таймингам. - web-vitals — обёртка для отправки LCP/INP/CLS в аналитику.
import { onLCP, onINP, onCLS } from "web-vitals";
function send(metric) {
navigator.sendBeacon("/rum", JSON.stringify(metric));
}
onLCP(send);
onINP(send);
onCLS(send);
| Lab | Field (RUM) | |
|---|---|---|
| Источник | синтетика | реальные юзеры |
| Инструмент | Lighthouse | web-vitals / CrUX |
| Польза | отладка, CI | реальный опыт |
⚠️ Частая ошибка: оптимизировать только по 100/100 в Lighthouse. Lab-прогон не покажет реальный INP — у пользователей на слабых телефонах метрики могут быть совсем другими.
10Как улучшить runtime-производительность скролла: requestAnimationFrame, debounce/throttle, passive-слушатели?
middle
Короткий ответ: Синхронизируйте визуальные обновления с кадрами через requestAnimationFrame, ограничивайте частоту обработчиков scroll/resize через throttle/debounce, помечайте слушатели { passive: true }, чтобы не блокировать скролл, и дробите длинные задачи (>50 мс), чтобы не страдал INP.
Подробно:
- requestAnimationFrame — выполняет колбэк перед следующей перерисовкой (~60 fps), избегая лишних layout и разрывов кадра.
- throttle / debounce — throttle ограничивает частоту вызова (скролл), debounce ждёт паузы (поиск, resize).
- passive: true — обещает не вызывать
preventDefault, поэтому браузер скроллит, не дожидаясь обработчика. - Длинные задачи — задачи дольше 50 мс блокируют main thread и портят INP; разбивайте их (
scheduler.yield,setTimeout).
let ticking = false;
window.addEventListener("scroll", () => {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => {
updateHeader(window.scrollY); // читаем/пишем layout один раз за кадр
ticking = false;
});
}, { passive: true });
⚠️ Частая ошибка: делать тяжёлые вычисления и чтение layout (offsetTop) прямо в обработчике scroll без rAF и throttle — это вызывает forced reflow на каждое событие и убивает плавность.
Источники
Источники и редакционная политика
Материалы RecallDeck сопоставлены с официальной документацией и открытыми публикациями компаний, когда первичный источник доступен. Мы не связаны с упомянутыми работодателями, не публикуем конфиденциальные задания и не продаём места в подборках. Формат найма может меняться — уточняйте его у рекрутера.
От чтения к воспроизведению
Отрепетируйте полный цикл интервью.
RecallDeck возвращает сложные темы по расписанию и помогает удерживать в памяти язык, SQL, архитектуру и поведенческие истории.