System design для frontend разработчика: как мыслить системно, а не только компонентами
Практический разбор system design для frontend разработчика: границы ответственности, данные, производительность, ошибки и сильный ответ на интервью.
- Введение
- Что именно означает system design во фронтенде
- Базовая модель: из каких слоев состоит frontend-система
- Архитектурный разбор: проектируем большой экран каталога
- Контекст задачи
- Схема компонентов и ролей
- Поток запроса и события
- Где здесь точки отказа
- Где должны жить данные и состояние
- Сравнение подходов
- Разбор производительности: где frontend system design обычно проигрывает
- Production pitfalls
- 1. Один store на все
- 2. Server state копируют в клиентское хранилище
- 3. shared превращается в черный ящик
- 4. Нет стратегии деградации
- Практики, которые реально помогают
- Частые ошибки
- Как отвечать на интервью
- FAQ
- С чего начать изучение system design фронтендеру
- Нужно ли frontend разработчику знать backend-часть system design
- Когда микрофронтенды действительно оправданы
- Как понять, что проблема архитектурная, а не локальная
- Что самое важное в frontend system design ответе
- Итоги
Введение
System design для frontend разработчика часто недооценивают, потому что само словосочетание ассоциируется с базами данных, очередями и балансировщиками. Во фронтенде вопросы другие, но природа та же: как система будет расти, где проходят границы ответственности, что станет узким местом и как продукт переживет ошибки сети, рост данных и усложнение сценариев.
Для frontend-инженера системное мышление обычно проявляется не в выборе между Kafka и RabbitMQ, а в более приземленных решениях: где хранить состояние, как организовать модули, как грузить данные и чанки, как не заблокировать основной поток браузера (main thread), как изолировать деградацию и как сделать так, чтобы экран на 50 тысяч строк данных не развалился от первой новой фичи. Если эту рамку не держать в голове, даже хороший код на уровне компонентов начинает плохо жить в масштабе.
Эта тема хорошо дополняет материалы про React архитектуру больших приложений, Feature-Sliced Design и React performance profiling. Вместе они показывают, что frontend-система ломается не в одном месте, а на стыке модулей, данных, сети и производительности.
Больше вопросов в Telegram
Ежедневные разборы и реальные кейсы с интервью.
Что именно означает system design во фронтенде
Если убрать лишнюю торжественность, system design во фронтенде отвечает на семь вопросов:
- Какие крупные части есть у системы и кто за что отвечает.
- Где живут разные типы данных и кто является источником истины.
- Как проходит пользовательское событие через интерфейс, клиентскую логику и API.
- Какие ограничения задают браузер, сеть, устройство и команда.
- Где будут узкие места по задержкам, памяти и стоимости поддержки.
- Как система деградирует при сбоях.
- Как все это будет масштабироваться по коду, людям и сценариям.
Поэтому сильный системный ответ в контексте фронтенда почти никогда не начинается со слов "я бы выбрал 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 живет рядом с конкретной частью интерфейса: открыта ли панель, какая строка выбрана, какой таб активен.
Поток запроса и события
- Пользователь вводит поисковый запрос.
- Фича фильтров обновляет локальное состояние поля сразу, чтобы интерфейс оставался отзывчивым.
- После debounce фильтры сериализуются в URL и ключ запроса.
- Query-слой запрашивает данные и кладет их в кэш.
- Таблица получает только готовую выборку и метаданные пагинации.
- При массовом изменении статуса UI показывает optimistic update, но сервер остается источником истины.
- После успешной мутации инвалидируются только релевантные ключи, а не весь экран.
Где здесь точки отказа
- слишком широкий 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 разработчика обычно строится в таком порядке:
- Коротко уточнить контекст: что за продукт, сколько данных, какие пользователи, web или mobile web, нужен ли SEO, есть ли SSR.
- Нарисовать 5-7 крупных блоков: клиент, API, кэш, роутинг, ключевые модули экрана, точки отказа.
- Разделить типы состояния и назвать источник истины для каждого.
- Объяснить поток одного критичного сценария: поиск, редактирование, checkout, чат.
- Отдельно проговорить производительность и деградацию.
- В конце назвать 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
Читайте также
frontend
Как проектировать масштабируемый React frontend: архитектура, состояние и границы модулей
Практический разбор того, как проектировать масштабируемый React frontend: модули, state management, performance, типичные ошибки и сильный ответ на интервью.
frontend
React архитектура больших приложений: как не утонуть в слоях, состояниях и фичах
Практический разбор React архитектуры больших приложений: слои, feature-модули, state management, performance, ошибки и критерии выбора.
frontend
Feature-Sliced Design для React проектов: когда FSD помогает, а когда усложняет код
Практический разбор Feature-Sliced Design для React проектов: слои, public API, правила зависимостей, типичные ошибки, производительность и ответы для интервью.