Связывайте ответ с браузером, доступностью, производительностью и пользовательским состоянием — именно там видна зрелость frontend-инженера.
Вопросы и ответы
8 подробных ответов
01Как подойти к задаче на frontend system design на собеседовании?
senior
Короткий ответ: Не бросайся рисовать UI. Двигайся по фреймворку RADIO: сначала проясни требования, затем зафиксируй контракт данных/API, потом компонентную архитектуру, состояние и в конце — сквозные аспекты (производительность, доступность, состояния ошибок).
Подробно:
- Requirements (требования) — функциональные и нефункциональные: кто пользователь, сколько данных, офлайн, целевые устройства, ключевые метрики (TTI, частота обновлений). Запиши их явно.
- Architecture / API — нарисуй высокоуровневые блоки и контракт данных: эндпоинты, форма ответа, пагинация (cursor vs offset), кто владеет состоянием.
- Data model — нормализованный server state и отдельный client/UI state; что кэшируем и как инвалидируем.
- Interface (компоненты) — дерево компонентов, props и события, переиспользуемость, границы загрузки.
- Optimizations — виртуализация, code splitting, дебаунс, отмена гонок запросов, a11y, loading/empty/error.
Clarify ──► API/Data ──► Components ──► State ──► Cross-cutting
requirements contract tree server/UI perf · a11y · errors
⚠️ Частая ошибка: прыгать к вёрстке UI, не проговорив требования и контракт данных — интервьюер ждёт именно структурированного движения сверху вниз.
02Как спроектировать компонентную архитектуру: композиция, container/presentational, compound-компоненты?
senior
Короткий ответ: Предпочитай композицию наследованию, разделяй логику и представление, а для связанных групп компонентов используй compound-паттерн через context — это убирает prop drilling и даёт гибкий, переиспользуемый API.
Подробно:
- Композиция вместо наследования — собирай UI из мелких компонентов и
children/слотов, не плоди иерархии классов. - Container vs presentational — контейнер тянет данные и держит состояние, presentational получает props и только рисует; так проще тестировать и переиспользовать.
- Compound components —
<Tabs><Tab/></Tabs>делят неявное состояние через context, а не через десяток props. - Против prop drilling — поднимай состояние ровно настолько, насколько нужно; глубокую передачу решай через context или композицию, а не сквозными props.
const TabsCtx = createContext<TabsState | null>(null);
function Tabs({ defaultValue, children }: TabsProps) {
const [value, setValue] = useState(defaultValue);
return <TabsCtx value={{ value, setValue }}>{children}</TabsCtx>;
}
Tabs.List = TabList; // compound: общее состояние через context
Tabs.Trigger = TabTrigger;
Tabs.Panel = TabPanel;
⚠️ Частая ошибка: превращать один компонент в «бога» с 20 props на все случаи — лучше расщепить на compound-части с понятным контрактом.
03Как организовать состояние в крупном SPA: server state, client/UI state, кэш и нормализация?
senior
Короткий ответ: Раздели состояние на server state (данные с бэкенда) и client/UI state (локальный интерфейс). Server state отдай специализированному кэшу (React Query/SWR/RTK Query), а реляционные данные нормализуй по id, чтобы не было рассинхрона.
Подробно:
- Server state — асинхронный, разделяемый, устаревает; ему нужны кэш, дедупликация запросов, фоновая ре-валидация и инвалидация. Не держи его в Redux руками.
- Client/UI state — модалки, табы, форма, тема; живёт в
useState/контексте/легковесном сторе. - Нормализация — храни сущности как
{ byId, allIds }, ссылайся по id, а не вкладывай дубликаты; обновление в одном месте обновляет везде. - Инвалидация кэша — по ключам после мутаций; задавай
staleTime, чтобы не бить сеть зря.
| Аспект | Server state | Client/UI state |
|---|---|---|
| Источник | бэкенд | сам клиент |
| Свежесть | устаревает, ре-валидация | всегда актуально |
| Инструмент | React Query / SWR | useState / context |
| Кэш | да, по ключам | обычно нет |
⚠️ Частая ошибка: складывать ответы API в глобальный Redux и вручную писать loading/error/кэш — это переизобретение библиотек server state.
04Как спроектировать переиспользуемую библиотеку компонентов / дизайн-систему?
senior
Короткий ответ: В основе — токены (цвет, типографика, spacing) и компоненты с минимальным, предсказуемым API: варианты вместо булевых флагов, полный набор состояний, доступность по умолчанию и семантическое версионирование.
Подробно:
- Токены — единый источник правды для цвета/spacing/радиусов как переменные; темизация и контраст строятся на них.
- API компонента —
variant/sizeкак объединения строк, а не россыпь boolean; разумные дефолты; проброс...restиrefна корневой элемент. - Доступность — корректные роли/ARIA, фокус-кольца, навигация с клавиатуры встроены, а не на потребителе.
- Варианты и состояния — hover/focus/disabled/loading/error предусмотрены сразу.
- Версионирование — semver, документация и changelog; ломающие изменения API — это major.
type ButtonProps = {
variant?: 'primary' | 'secondary' | 'ghost'; // не isPrimary/isGhost
size?: 'sm' | 'md' | 'lg';
loading?: boolean;
} & React.ButtonHTMLAttributes<HTMLButtonElement>; // проброс остального
⚠️ Частая ошибка: разрастание булевых флагов (isPrimary, isLarge, isGhost) вместо единого variant — взаимоисключающие состояния становятся возможны, а API — хрупким.
05Как спроектировать ленту с бесконечной прокруткой (новостной feed)?
senior
Короткий ответ: Cursor-пагинация для подгрузки, виртуализация для отрисовки только видимых элементов, кэш страниц по курсору, IntersectionObserver как триггер дозагрузки и оптимистичные апдейты для лайков/действий. Не забудь восстановление позиции скролла.
Подробно:
- Пагинация курсором —
?cursor=<id>&limit=20; устойчива к вставкам/удалениям, в отличие от offset, где элементы дублируются или пропадают. - Виртуализация — рендерим только видимое окно (react-virtual/virtuoso), иначе тысячи DOM-узлов убивают память и скролл.
- Триггер дозагрузки — IntersectionObserver на сентинеле в конце списка вместо слушателя
scroll. - Кэш и оптимизм — страницы кэшируются по курсору; лайки применяем оптимистично с откатом при ошибке.
- Scroll restoration — сохраняем якорь/offset, чтобы возврат с детального экрана не прыгал.
[scroll] ─► IntersectionObserver(sentinel)
│ fetch(?cursor=last_id)
▼
┌─────────────────────────┐
│ cache: page1·page2·page3 │ ──► virtualized window
└─────────────────────────┘ (renders ~visible rows)
⚠️ Частая ошибка: offset-пагинация для живой ленты — при новых записях сверху страницы сдвигаются, и пользователь видит дубли или пропуски.
06Как спроектировать autocomplete / typeahead (поисковые подсказки)?
senior
Короткий ответ: Дебаунс ввода, кэш результатов по запросу, обязательная отмена устаревших запросов через AbortController (иначе медленный ответ перезапишет свежий), полная клавиатурная навигация и ARIA-роль combobox.
Подробно:
- Дебаунс — ждём ~200–300 мс паузы в наборе, чтобы не слать запрос на каждое нажатие.
- Отмена гонок — каждый новый ввод отменяет предыдущий fetch через
AbortController; так ответ на «react» не перетрёт ответ на «react native». - Кэш — мемоизируем результаты по строке запроса; повтор — без сети.
- Доступность —
role="combobox",aria-activedescendant, стрелки/Enter/Esc, объявление количества результатов. - Состояния — loading, пусто («ничего не найдено»), ошибка с возможностью повтора.
useEffect(() => {
const ctrl = new AbortController();
const t = setTimeout(async () => {
try {
const res = await fetch(`/search?q=${query}`, { signal: ctrl.signal });
setItems(await res.json());
} catch (e) {
if (e.name !== 'AbortError') setError(e); // отмена — не ошибка
}
}, 250); // дебаунс
return () => { clearTimeout(t); ctrl.abort(); }; // отмена устаревшего
}, [query]);
⚠️ Частая ошибка: не отменять запросы в гонке — старый ответ приходит позже нового и подменяет актуальные подсказки.
07Как реализовать realtime-обновления UI: polling vs SSE vs WebSocket?
senior
Короткий ответ: Выбор по направлению и частоте данных. Polling — для редких обновлений и простоты; SSE — для односторонних серверных push (лента, нотификации); WebSocket — для двунаправленного низколатентного обмена (чат, коллаборация). Сверху — оптимистичный UI и реконсиляция при приходе серверного состояния.
Подробно:
- Polling — клиент сам опрашивает через интервал; просто, но создаёт лишние запросы и лаг; long-polling сглаживает.
- SSE — один HTTP-стрим сервер→клиент, авто-reconnect, поверх обычного HTTP; только в одну сторону.
- WebSocket — постоянное двунаправленное соединение, минимальная задержка; дороже в инфраструктуре, нужен heartbeat/reconnect.
- Оптимизм и реконсиляция — применяем изменение локально сразу, при подтверждении/конфликте сверяемся с серверным состоянием и откатываем при расхождении.
| Критерий | Polling | SSE | WebSocket |
|---|---|---|---|
| Направление | client→server | server→client | двунаправленное |
| Задержка | высокая | низкая | очень низкая |
| Сложность | низкая | средняя | высокая |
| Когда | редкие апдейты | фид, нотификации | чат, коллаборация |
⚠️ Частая ошибка: тянуть WebSocket туда, где хватило бы SSE или периодического polling — лишняя сложность инфраструктуры и обработки reconnect без реальной потребности в двунаправленности.
08Как сделать фронтенд устойчивым: error boundaries, ретраи, деградация и состояния загрузки/ошибки?
senior
Короткий ответ: Локализуй сбои error boundary'ями, чтобы падал виджет, а не всё приложение; повторяй транзиентные запросы с экспоненциальным backoff; всегда проектируй четыре состояния (loading/empty/error/success) и деградируй мягко при отказе некритичных частей и офлайне.
Подробно:
- Error boundaries — оборачивай рискованные поддеревья; вместо белого экрана — fallback с кнопкой «повторить».
- Retry с backoff — повторяй сетевые ошибки/5xx с экспоненциальной задержкой и jitter; 4xx не ретраим.
- Состояния UI — loading (скелетоны), empty (понятный пустой экран), error (с действием), success — все спроектированы, а не забыты.
- Graceful degradation — упавший некритичный блок не валит страницу; ядро функциональности остаётся.
- Offline — детектим
navigator.onLine, кэшируем через Service Worker, ставим мутации в очередь до восстановления связи.
class ErrorBoundary extends React.Component<Props, { error: Error | null }> {
state = { error: null };
static getDerivedStateFromError(error: Error) { return { error }; }
componentDidCatch(error, info) { logToService(error, info); } // телеметрия
render() {
return this.state.error
? <Fallback onRetry={() => this.setState({ error: null })} />
: this.props.children; // изолирован сбой поддерева
}
}
⚠️ Частая ошибка: рисовать только happy path и забывать loading/empty/error — реальный пользователь первым делом ловит именно эти состояния.
Источники
Источники и редакционная политика
Материалы RecallDeck сопоставлены с официальной документацией и открытыми публикациями компаний, когда первичный источник доступен. Мы не связаны с упомянутыми работодателями, не публикуем конфиденциальные задания и не продаём места в подборках. Формат найма может меняться — уточняйте его у рекрутера.
От чтения к воспроизведению
Отрепетируйте полный цикл интервью.
RecallDeck возвращает сложные темы по расписанию и помогает удерживать в памяти язык, SQL, архитектуру и поведенческие истории.