Коротка відповідь. Frontend system design – це не про малювання квадратиків із «load balancer». Це розмова про те, як ти проєктуєш клієнтський застосунок: дані та API, стан, рендеринг і кеш, продуктивність, доступність, стани помилок. Виграшна стратегія – йти по явному фреймворку, вголос називати компроміси і не ускладнювати без запиту інтерв'юера. Нижче – сам фреймворк і повний приклад розбору стрічки з нескінченним скролом.

Що перевіряють насправді

Інтерв'юер дивиться не на «правильну архітектуру» (її не існує), а на чотири речі:

  • чи вмієш ти звужувати розмиту задачу до чітких вимог;
  • чи бачиш компроміси, а не один завчений варіант;
  • чи думаєш про несчасливі сценарії: помилки, повільну мережу, порожні стани;
  • чи можеш спілкуватися як із колегою: структура, малі кроки, реакція на підказки.

Це той самий набір, який відрізняє Senior від Middle у щоденній роботі – докладніше про цю різницю в статті про перехід Middle → Senior.

Фреймворк відповіді: 8 кроків

  1. Вимоги. Хто користувач? Які ключові сценарії? Що точно поза скоупом? Реальний час чи можна оновлювати за запитом? Які пристрої й мережі?
  2. API і дані. Форма даних, пагінація (offset чи cursor), що віддає бекенд, що доводиться агрегувати на клієнті.
  3. Стан. Що є server state (кешовані відповіді API), що – client state (фільтри, введення). Де він живе і хто ним володіє.
  4. Рендеринг і кеш. SSR/CSR/гібрид і чому; що кешуємо, коли інвалідовуємо, що робимо зі stale-даними.
  5. Performance. Що вантажимо одразу, що ліниво; списки – віртуалізація; зображення – розміри й формати; метрики, за якими стежимо (LCP, INP, CLS – визначення є на web.dev).
  6. Доступність. Клавіатура, фокус, семантика, анонси динамічних змін.
  7. Стани помилок. Loading / empty / error / partial для кожного блоку даних; ретраї; офлайн-поведінка, якщо релевантно.
  8. 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-раунді

  1. Малювати без вимог. Перші хвилини без жодного питання – найчастіший провал раунду.
  2. Стартувати з технологій. «Візьму Redux і GraphQL» до того, як зрозуміла форма даних, – рішення без задачі.
  3. Щасливий шлях і все. Жодного слова про помилки мережі, порожні стани, повільні пристрої.
  4. Перепроєктування. Віртуалізація, офлайн-режим і мікрофронтенди у перші п'ять хвилин простої задачі. Складність має з'являтися у відповідь на вимогу, а не про запас.
  5. Мовчазні рішення. Кожне «беру X» без «замість Y, тому що…» – втрачений бал.

Рубрика самооцінки

Після тренування оціни себе за чотирма пунктами: (1) чи поставив я хоч три уточнювальні питання до того, як малювати; (2) чи назвав хоча б один компроміс на кожне велике рішення; (3) чи спроєктував error/empty/loading, не чекаючи підказки; (4) чи вклався в таймінг, лишивши час на питання. Два «ні» – значить, тренуватися ще рано на реальній співбесіді.

Design-раунд тісно пов'язаний з архітектурною розмовою про компоненти й межі відповідальності – це окрема тема в React-архітектурі для Senior. А якщо design-питання прилітають у контексті бекенда, подивись розбір Node.js-співбесіди.

Практичне завдання

Візьми продукт, яким користуєшся щодня, і спроєктуй один його екран за фреймворком вище – письмово, за 40 хвилин, з таймером. Потім перечитай як інтерв'юер: де ти прийняв рішення мовчки, без альтернативи? Саме ці місця на реальній співбесіді викликають питання «а чому так?», на які боляче не мати відповіді.