Коротка відповідь. Frontend system design – це не про малювання квадратиків із «load balancer». Це розмова про те, як ти проєктуєш клієнтський застосунок: дані та API, стан, рендеринг і кеш, продуктивність, доступність, стани помилок. Виграшна стратегія – йти по явному фреймворку, вголос називати компроміси і не ускладнювати без запиту інтерв'юера. Нижче – сам фреймворк і повний приклад розбору стрічки з нескінченним скролом.
Що перевіряють насправді
Інтерв'юер дивиться не на «правильну архітектуру» (її не існує), а на чотири речі:
- чи вмієш ти звужувати розмиту задачу до чітких вимог;
- чи бачиш компроміси, а не один завчений варіант;
- чи думаєш про несчасливі сценарії: помилки, повільну мережу, порожні стани;
- чи можеш спілкуватися як із колегою: структура, малі кроки, реакція на підказки.
Це той самий набір, який відрізняє Senior від Middle у щоденній роботі – докладніше про цю різницю в статті про перехід Middle → Senior.
Фреймворк відповіді: 8 кроків
- Вимоги. Хто користувач? Які ключові сценарії? Що точно поза скоупом? Реальний час чи можна оновлювати за запитом? Які пристрої й мережі?
- API і дані. Форма даних, пагінація (offset чи cursor), що віддає бекенд, що доводиться агрегувати на клієнті.
- Стан. Що є server state (кешовані відповіді API), що – client state (фільтри, введення). Де він живе і хто ним володіє.
- Рендеринг і кеш. SSR/CSR/гібрид і чому; що кешуємо, коли інвалідовуємо, що робимо зі stale-даними.
- Performance. Що вантажимо одразу, що ліниво; списки – віртуалізація; зображення – розміри й формати; метрики, за якими стежимо (LCP, INP, CLS – визначення є на web.dev).
- Доступність. Клавіатура, фокус, семантика, анонси динамічних змін.
- Стани помилок. Loading / empty / error / partial для кожного блоку даних; ретраї; офлайн-поведінка, якщо релевантно.
- Observability. Як дізнаємось, що в користувачів щось зламалося: логування помилок, метрики продуктивності, алерти.
Не обов'язково проходити всі вісім глибоко – назви структуру на початку і запитай інтерв'юера, куди пірнати. Це сам по собі сильний сигнал.
Приклад розбору: стрічка з нескінченним скролом
Задача: «спроєктуй стрічку постів, як у соцмережі».
Крок 1 – вимоги (2–3 хвилини, вголос)
Питання, які варто поставити: стрічка персоналізована чи однакова для всіх? Потрібен реальний час чи оновлення при заході? Чи є взаємодії (лайк, коментар)? Мобільні користувачі з повільною мережею – цільова аудиторія? Припустимо відповіді: персоналізована, реальний час не потрібен, є лайк, мобільні важливі.
Крок 2 – API
Cursor-пагінація замість offset: у стрічку постійно додаються нові пости, offset «з'їжджає» і дає дублікати. Контракт:
type FeedResponse = {
items: Post[];
nextCursor: string | null; // null – кінець стрічки
};
type Post = {
id: string;
author: { id: string; name: string; avatarUrl: string };
text: string;
media: { url: string; width: number; height: number } | null;
likeCount: number;
likedByMe: boolean;
createdAt: string;
};width/height медіа у відповіді – не дрібниця: без них картки стрибають під час завантаження зображень (CLS).
Крок 3 – стан
Server state: сторінки стрічки, кешовані за cursor. Client state: позиція скролу, чернетка коментаря. Лайк – оптимістичне оновлення: міняємо UI одразу, відкочуємо при помилці запиту.
Крок 4 – компонентна структура
FeedPage
├── FeedList (керує сторінками, sentinel для догрузки)
│ └── PostCard (чистий, отримує все через props)
├── NewPostsBanner («З'явилися нові пости» – за кліком, не зсуваючи стрічку)
└── FeedErrorState / FeedSkeletonДогрузка – IntersectionObserver на sentinel-елементі внизу; кнопка «Показати ще» як fallback для доступності.
Крок 5 – довгий список
Нескінченний скрол означає, що DOM росте необмежено. Називаємо проблему і два рівні рішення: простіший – відвантажувати далекі сторінки, зберігаючи їхню висоту плейсхолдером; повніший – віртуалізація (рендеримо лише видиме вікно елементів). На співбесіді достатньо пояснити принцип і чесно сказати, що в реальному проєкті взяв би перевірену бібліотеку, а не писав із нуля.
Кроки 6–8 – стани, доступність, спостережуваність
Кожна сторінка стрічки має власні loading/error стани: помилка догрузки не має вбивати вже показане – показуємо існуючі пости + «Не вдалося завантажити, спробувати ще». Порожня стрічка – окремий екран з дією. Доступність: пости – семантичні <article>, лайк – <button> з aria-pressed, нові елементи не крадуть фокус. Спостережуваність: репортинг JS-помилок і Web Vitals у аналітику.
Таблиця компромісів (корисно проговорити вголос)
| Рішення | Альтернатива | Ціна обраного |
|---|---|---|
| Cursor-пагінація | Offset | Складніше «стрибнути на сторінку N» – стрічці не потрібно |
| Оптимістичний лайк | Чекати відповідь сервера | Потрібен відкат при помилці |
| IntersectionObserver | Скрол-listener | Майже жодної: observer простіший і дешевший |
| Віртуалізація | Рендер усього | Складність, крихкість динамічних висот |
| CSR стрічки після SSR-оболонки | Повний SSR кожної сторінки | Перший контент трохи пізніше; для приватної стрічки SEO не потрібне |
SSR чи CSR: як аргументувати вибір рендерингу
Це питання виринає майже в кожному design-раунді, тому підготуй логіку заздалегідь:
- SSR виграє, коли перший контент критичний (публічні сторінки, SEO, повільні пристрої): HTML приходить готовим, до виконання JS.
- CSR достатній для застосунків за логіном: SEO не потрібне, а після першого завантаження навігація дешевша.
- Гібрид – найчастіша чесна відповідь: SSR-оболонка і перша порція даних, далі клієнтські переходи. Для нашої стрічки: якщо вона за логіном – SSR повної стрічки не окупається; якщо публічна – перший екран варто рендерити на сервері.
Аргументуй завжди через користувача й вимоги, а не через «так модно»: інтерв'юер чує різницю миттєво.
Типові помилки на design-раунді
- Малювати без вимог. Перші хвилини без жодного питання – найчастіший провал раунду.
- Стартувати з технологій. «Візьму Redux і GraphQL» до того, як зрозуміла форма даних, – рішення без задачі.
- Щасливий шлях і все. Жодного слова про помилки мережі, порожні стани, повільні пристрої.
- Перепроєктування. Віртуалізація, офлайн-режим і мікрофронтенди у перші п'ять хвилин простої задачі. Складність має з'являтися у відповідь на вимогу, а не про запас.
- Мовчазні рішення. Кожне «беру X» без «замість Y, тому що…» – втрачений бал.
Рубрика самооцінки
Після тренування оціни себе за чотирма пунктами: (1) чи поставив я хоч три уточнювальні питання до того, як малювати; (2) чи назвав хоча б один компроміс на кожне велике рішення; (3) чи спроєктував error/empty/loading, не чекаючи підказки; (4) чи вклався в таймінг, лишивши час на питання. Два «ні» – значить, тренуватися ще рано на реальній співбесіді.
Design-раунд тісно пов'язаний з архітектурною розмовою про компоненти й межі відповідальності – це окрема тема в React-архітектурі для Senior. А якщо design-питання прилітають у контексті бекенда, подивись розбір Node.js-співбесіди.
Практичне завдання
Візьми продукт, яким користуєшся щодня, і спроєктуй один його екран за фреймворком вище – письмово, за 40 хвилин, з таймером. Потім перечитай як інтерв'юер: де ти прийняв рішення мовчки, без альтернативи? Саме ці місця на реальній співбесіді викликають питання «а чому так?», на які боляче не мати відповіді.