System design для frontend разработчика: как мыслить системно, а не только компонентами

Практический разбор system design для frontend разработчика: границы ответственности, данные, производительность, ошибки и сильный ответ на интервью.

25 марта 2026 г.18 минLexicon Team

Введение

System design для frontend разработчика часто недооценивают, потому что само словосочетание ассоциируется с базами данных, очередями и балансировщиками. Во фронтенде вопросы другие, но природа та же: как система будет расти, где проходят границы ответственности, что станет узким местом и как продукт переживет ошибки сети, рост данных и усложнение сценариев.

Для frontend-инженера системное мышление обычно проявляется не в выборе между Kafka и RabbitMQ, а в более приземленных решениях: где хранить состояние, как организовать модули, как грузить данные и чанки, как не заблокировать основной поток браузера (main thread), как изолировать деградацию и как сделать так, чтобы экран на 50 тысяч строк данных не развалился от первой новой фичи. Если эту рамку не держать в голове, даже хороший код на уровне компонентов начинает плохо жить в масштабе.

Эта тема хорошо дополняет материалы про React архитектуру больших приложений, Feature-Sliced Design и React performance profiling. Вместе они показывают, что frontend-система ломается не в одном месте, а на стыке модулей, данных, сети и производительности.

Больше вопросов в Telegram

Ежедневные разборы и реальные кейсы с интервью.

Подписаться

Что именно означает system design во фронтенде

Если убрать лишнюю торжественность, system design во фронтенде отвечает на семь вопросов:

  1. Какие крупные части есть у системы и кто за что отвечает.
  2. Где живут разные типы данных и кто является источником истины.
  3. Как проходит пользовательское событие через интерфейс, клиентскую логику и API.
  4. Какие ограничения задают браузер, сеть, устройство и команда.
  5. Где будут узкие места по задержкам, памяти и стоимости поддержки.
  6. Как система деградирует при сбоях.
  7. Как все это будет масштабироваться по коду, людям и сценариям.

Поэтому сильный системный ответ в контексте фронтенда почти никогда не начинается со слов "я бы выбрал React Query и Zustand". Сначала нужно объяснить контекст: что за продукт, какие сценарии критичны, где много данных, где важен быстрый отклик, есть ли SSR, есть ли офлайн, насколько часто данные меняются и сколько команд будут трогать кодовую базу.

Базовая модель: из каких слоев состоит frontend-система

Универсальной схемы нет, но для большинства веб-приложений полезно разделять хотя бы такие зоны:

  • shell приложения: роутинг, провайдеры, сессия, глобальная инициализация;
  • экран или route-модуль: композиция конкретного сценария;
  • feature-слой: пользовательские действия вроде фильтров, редактирования, оплаты;
  • entity-слой: устойчивые доменные сущности и их представления;
  • data layer: запросы, кэш, инвалидация, адаптеры ответа сервера;
  • shared-инфраструктура: UI-kit, утилиты, конфиг, транспорт, логирование.

Главное здесь не названия папок, а правила зависимостей. Верхние слои собирают нижние, но не наоборот. Экран знает, какие виджеты отрисовать. Фича знает, как обработать действие пользователя. Сущность знает, как представить доменный объект. Инфраструктура не должна внезапно знать о скидках, тарифах или правах доступа.

Когда эти границы размываются, появляются типичные симптомы: страница на 700 строк orchestration-кода, shared с бизнес-логикой, один store на все случаи жизни и споры о том, "почему модуль фильтров импортирует внутренности модуля корзины". Это уже не локальная проблема организации файлов, а системная проблема.

Архитектурный разбор: проектируем большой экран каталога

Контекст задачи

Представим B2B-экран каталога товаров:

  • таблица на несколько тысяч строк;
  • фильтры и сортировка;
  • поиск с debounce;
  • карточка товара в сайдбаре;
  • массовое изменение статусов;
  • права доступа по ролям;
  • частые обновления остатков и цен;
  • мобильная версия с упрощенным layout.

Такой экран удобен для system design, потому что здесь быстро видны и поток данных, и цена ошибок.

Схема компонентов и ролей

  • page собирает layout, URL-параметры, split по зонам экрана.
  • features/catalog-filters управляет фильтрами и синхронизацией с URL.
  • features/bulk-actions отвечает за массовые действия и optimistic update.
  • entities/product хранит типы, форматтеры, базовые UI-представления товара.
  • query-слой управляет server state, кэшем, повторными запросами и инвалидацией.
  • локальный UI-state живет рядом с конкретной частью интерфейса: открыта ли панель, какая строка выбрана, какой таб активен.

Поток запроса и события

  1. Пользователь вводит поисковый запрос.
  2. Фича фильтров обновляет локальное состояние поля сразу, чтобы интерфейс оставался отзывчивым.
  3. После debounce фильтры сериализуются в URL и ключ запроса.
  4. Query-слой запрашивает данные и кладет их в кэш.
  5. Таблица получает только готовую выборку и метаданные пагинации.
  6. При массовом изменении статуса UI показывает optimistic update, но сервер остается источником истины.
  7. После успешной мутации инвалидируются только релевантные ключи, а не весь экран.

Где здесь точки отказа

  • слишком широкий provider или store, который перерендеривает полстраницы;
  • дублирование server state в клиентском хранилище;
  • фильтры, живущие отдельно от URL, из-за чего ломаются refresh и share link;
  • таблица без виртуализации при больших наборах данных;
  • тяжелая синхронная обработка данных прямо в render;
  • отсутствие явной стратегии деградации, когда часть API недоступна.

Именно это отличает системный ответ от ответа на уровне компонента. Нужно не просто "нарисовать таблицу", а объяснить, где система начнет болеть и как это предотвратить заранее.

Где должны жить данные и состояние

Во frontend system design почти всегда полезно разделять минимум четыре вида состояния:

  • UI-state: модалки, открытые панели, локальные инпуты, активные табы;
  • server state: данные из API, завязанные на кэш, фоновое обновление и инвалидацию;
  • URL state: фильтры, сортировка, пагинация, состояние навигации;
  • workflow state: длинный пользовательский сценарий вроде многошаговой формы или checkout.

Самая частая ошибка - складывать все это в один контейнер "для единообразия". Снаружи решение кажется аккуратным, но затем одна мелкая смена activeTab начинает будить подписчиков таблицы, серверные ответы устаревают, а в коде появляются флаги вроде hasInitializedFromServer.

Ниже пример нормальной границы, где локальное состояние остается локальным, а server state не дублируется без причины:

type CatalogFilters = {
  search: string;
  status: "all" | "active" | "archived";
  page: number;
};

function CatalogPage() {
  const [searchDraft, setSearchDraft] = useState("");
  const [filters, setFilters] = useState<CatalogFilters>({
    search: "",
    status: "all",
    page: 1,
  });

  useEffect(() => {
    const id = setTimeout(() => {
      setFilters((prev) => ({ ...prev, search: searchDraft, page: 1 }));
    }, 300);

    return () => clearTimeout(id);
  }, [searchDraft]);

  const query = useProductsQuery(filters);

  return (
    <CatalogLayout
      searchDraft={searchDraft}
      onSearchDraftChange={setSearchDraft}
      filters={filters}
      onFiltersChange={setFilters}
      products={query.data?.items ?? []}
      total={query.data?.total ?? 0}
      isLoading={query.isFetching}
    />
  );
}

Здесь нет ложной "универсальности". Черновик поиска локален, потому что он нужен только текущему экрану. Фильтры участвуют в запросе. Серверные данные живут в query-слое. Такая декомпозиция уменьшает радиус обновлений и делает источник истины очевидным.

Сравнение подходов

ПодходГде работает хорошоОграниченияКогда выбирать
Локальный state рядом с featureФормы, модалки, панели, короткоживущий UIТрудно делить между удаленными частями дереваКогда состояние не выходит за границы сценария
URL как источник истиныФильтры, сортировка, пагинация, deep linkНе все удобно сериализовать в адресКогда важно переживать refresh и делиться ссылкой
Отдельный server-state слойКэш, фоновое обновление, optimistic updateНельзя смешивать с временным UI-stateКогда источник истины на сервере
Глобальный store с селекторамиДолгоживущее клиентское состояние, общий workflowНужны дисциплина модулей и ownershipКогда состояние реально разделяется между несколькими зонами
SSR или React Server Components (серверные компоненты React)SEO, быстрый first paint, тяжелые initial fetchСложнее синхронизировать интерактивность и клиентский stateКогда критичны стартовая загрузка и поисковый трафик
МикрофронтендыНезависимые команды и жесткие границы поставкиДорогая интеграция, дубли инфраструктуры, сложнее UXКогда масштаб людей важнее простоты одной кодовой базы

На практике сильная система почти всегда гибридная. В одном приложении одновременно живут локальный state, URL state, серверный кэш и несколько явно ограниченных глобальных моделей.

Разбор производительности: где frontend system design обычно проигрывает

Во фронтенде производительность чаще ломается не из-за "медленного React", а из-за неправильных системных границ.

Типичные узкие места:

  • слишком широкий context или provider;
  • тяжелые вычисления прямо в render;
  • отсутствие виртуализации для длинных списков;
  • агрессивный refetch без учета сценария пользователя;
  • чрезмерно мелкий code splitting, который увеличивает network overhead;
  • синхронная сериализация больших структур на клиенте;
  • кэш, который инвалидируется слишком грубо.

Ниже пример анти-паттерна, который выглядит просто, но плохо масштабируется:

function ProductsTable({ products, search }: { products: Product[]; search: string }) {
  const visibleRows = products
    .filter((product) => product.title.toLowerCase().includes(search.toLowerCase()))
    .sort((a, b) => a.title.localeCompare(b.title))
    .map((product) => ({
      ...product,
      formattedPrice: formatPrice(product.price),
    }));

  return <Table rows={visibleRows} />;
}

Проблема не только в вычислениях. Здесь каждый ввод в поле поиска заставляет заново фильтровать, сортировать и адаптировать весь массив. Если данных много, лаг будет заметен еще до первого вопроса про useMemo.

Более системный вариант:

function useVisibleProducts(products: Product[], search: string) {
  return useMemo(() => {
    const normalizedSearch = search.trim().toLowerCase();

    return products
      .filter((product) => product.title.toLowerCase().includes(normalizedSearch))
      .sort((a, b) => a.title.localeCompare(b.title))
      .map((product) => ({
        id: product.id,
        title: product.title,
        formattedPrice: formatPrice(product.price),
      }));
  }, [products, search]);
}

function ProductsTableContainer() {
  const { data } = useProductsQuery();
  const [search, setSearch] = useState("");
  const rows = useVisibleProducts(data?.items ?? [], search);

  return <ProductsTable rows={rows} search={search} onSearchChange={setSearch} />;
}

Это не магия и не совет "всегда мемоизируйте". Здесь важна идея: тяжелая работа изолируется, контракт таблицы упрощается, а критерий оптимизации понятен. Если данных станет действительно много, следующим шагом будет не новая мемоизация, а виртуализация, пагинация или перенос части вычислений на сервер.

Если нужно глубже разбирать поиск узких мест, полезно держать под рукой React performance profiling и материал про code splitting и lazy loading. Они помогают не путать архитектурную ошибку с локальной микрооптимизацией.

Production pitfalls

1. Один store на все

Симптомы:

  • в store лежат и фильтры, и модалки, и серверные ответы, и права доступа;
  • простое действие пользователя вызывает длинную цепочку обновлений;
  • DevTools показывают каскад ререндеров на половине страницы.

Последствия в production: рост задержек на интерактивных сценариях и дорогостоящая поддержка. Исправление почти всегда связано не с новым state manager, а с разделением ответственности.

2. Server state копируют в клиентское хранилище

Симптомы:

  • после мутации нужно "синхронизировать" два источника истины;
  • появляются флаги isHydrated, hasPatched, shouldRefetch;
  • данные периодически устаревают после навигации или refresh.

Последствия: сложные race condition и ошибки, которые трудно воспроизводить. Намного надежнее оставить серверные данные в кэширующем слое и обновлять только то, что действительно принадлежит клиенту.

3. shared превращается в черный ящик

Симптомы:

  • там появляются shared/business, helpers/order, utils/catalog;
  • никто не может объяснить, почему доменный код лежит в общем слое;
  • новая фича начинает тянуть "удобную утилиту", которая знает слишком много о продукте.

Последствия: формально архитектура есть, фактически доменная логика просто спрятана под нейтральным именем.

4. Нет стратегии деградации

Симптомы:

  • экран либо полностью работает, либо полностью пустой;
  • ошибка одного второстепенного виджета ломает весь маршрут;
  • skeleton и retry не продуманы заранее.

Последствия: плохой UX и рост стоимости инцидентов. На system design интервью это важный маркер зрелости: хороший кандидат думает не только о happy path.

Прокачай React за 7 дней

20 вопросов и разборов по React Hooks.

Начать

Практики, которые реально помогают

  • Делить систему по ответственности, а не по типам файлов.
  • Явно различать UI-state, server state, URL state и workflow state.
  • Считать URL частью архитектуры, а не случайной деталью роутинга.
  • Локализовать тяжелые вычисления и не делать render местом для дорогой бизнес-логики.
  • Планировать деградацию: частичная загрузка, retry, fallback, error boundary.
  • Держать публичные API модулей короткими и предсказуемыми.
  • Мерить производительность по сценарию пользователя, а не по абстрактным ощущениям.
  • Продумывать rollout и rollback для рискованных изменений, особенно если меняются кэш, роутинг или схема данных.

Это хорошо сочетается с идеями из React архитектуры больших приложений и Feature-Sliced Design. Если нужен обзор смежных материалов по теме, можно пройтись и по frontend-разделу блога.

Частые ошибки

  • Начинать проектирование с библиотеки, а не с ограничений задачи.
  • Пытаться сделать одну "идеальную" модель состояния для всех сценариев.
  • Смешивать транспорт, доменную логику и UI в одном модуле.
  • Оптимизировать ререндеры до того, как измерена реальная проблема.
  • Игнорировать деградацию при частичных сбоях API.
  • Выбирать микрофронтенды как модный ответ на обычную проблему монорепы.

На интервью эти ошибки видны быстро. Если кандидат на каждый вопрос отвечает названием инструмента, но не может объяснить компромисс, значит системной рамки пока нет.

Как отвечать на интервью

Хороший ответ на system design для frontend разработчика обычно строится в таком порядке:

  1. Коротко уточнить контекст: что за продукт, сколько данных, какие пользователи, web или mobile web, нужен ли SEO, есть ли SSR.
  2. Нарисовать 5-7 крупных блоков: клиент, API, кэш, роутинг, ключевые модули экрана, точки отказа.
  3. Разделить типы состояния и назвать источник истины для каждого.
  4. Объяснить поток одного критичного сценария: поиск, редактирование, checkout, чат.
  5. Отдельно проговорить производительность и деградацию.
  6. В конце назвать 2-3 trade-off: почему не все держим в global store, почему не делаем микрофронтенды сразу, почему часть вычислений лучше увести на сервер.

Сильный ответ звучит примерно так: "Я бы сначала отделил server state от UI-state, вынес фильтры в URL, оставил локальные интерактивные флаги рядом с feature, а для таблицы сразу подумал о виртуализации и избирательной инвалидации кэша. Если экран критичен для SEO, посмотрел бы в сторону SSR или RSC, но не стал бы тянуть это в админский маршрут без пользы".

Такой ответ показывает зрелость намного лучше, чем длинный список библиотек. Интервьюеру важнее понять, как вы мыслите при росте системы, чем услышать слово Zustand раньше пятой минуты.

Подготовься к frontend system design в формате mock-интервью

Разберем архитектуру экрана, состояние, кэш, производительность и сильные trade-off на реальных вопросах уровня Middle и Senior.

Начать тренировку

FAQ

С чего начать изучение system design фронтендеру

С базового каркаса: типы состояния, границы модулей, поток данных, кэширование, производительность и деградация. После этого уже легче осмысленно читать про SSR, RSC, FSD, микрофронтенды и state management.

Нужно ли frontend разработчику знать backend-часть system design

Да, хотя бы на уровне контрактов, ограничений API, кэша, идемпотентности и влияния сети на UX. Хороший frontend system design всегда учитывает, что клиент не существует отдельно от сервера.

Когда микрофронтенды действительно оправданы

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

Как понять, что проблема архитектурная, а не локальная

Если один и тот же класс дефектов повторяется в разных местах: каскад ререндеров, дубли логики, сложные импорты через полпроекта, тяжелые страницы и конфликты ownership между командами, то это архитектурная проблема.

Что самое важное в frontend system design ответе

Показать причинно-следственную связь: какие ограничения есть, какое решение вы выбираете, какой компромисс принимаете и какие риски закладываете в дизайн заранее.

Итоги

System design для frontend разработчика - это не попытка притвориться backend-инженером. Это способ проектировать интерфейс как систему: с границами, источниками истины, узкими местами, деградацией и масштабированием. Чем раньше frontend-разработчик начинает так смотреть на продукт, тем легче ему расти от уровня "умею собирать компоненты" к уровню "умею удерживать сложное приложение в рабочем состоянии".

Если держать в голове слои ответственности, разделение состояния, поток данных и стоимость ошибок, многие решения становятся проще. И в реальной разработке, и на интервью это почти всегда видно сильнее, чем знание конкретной библиотеки.

Больше вопросов в Telegram

Ежедневные разборы и реальные кейсы с интервью.

Подписаться

Автор

Lexicon Team

Читайте также