Фронтенд-архитектура — не просто «как красиво нарисовать»
Проблема: Фронтенд больше не «прослойка между бэкендом и пользователем». Это самостоятельная система со своей архитектурой, которая может быть источником как преимуществ, так и катастрофических проблем. И да, это книга в первую очередь серверная, но игнорировать фронтенд-архитектуру в книге о веб-системах — всё равно что писать книгу об автомобилях и не упомянуть колёса.
#Введение: почему фронтенд — это архитектура
Лет пятнадцать назад фронтенд был простым. Отправил HTML с сервера, повесил jQuery-скрипт для анимации, добавил пару CSS-классов — готово. Фронтенд-разработчиком мог стать любой бэкендер, который знал, чем <div> отличается от <span>.
Всё изменилось.
Сегодня фронтенд-приложение — это распределённая система в миниатюре. У него есть свои состояния, свои потоки данных, своя логика, свой кэш, своя производительность, свои тесты. Браузер — это операционная система, а JavaScript-фреймворк — это рантайм. И да, у этой системы есть своя архитектура, со всеми вытекающими компромиссами.
Почему архитектору всего приложения важно понимать фронтенд-архитектуру? Три причины.
Первая: граница между фронтендом и бэкендом размывается. Server Components, Streaming SSR, Edge Functions — код, который раньше был строго серверным, теперь может быть и тем и другим. Решения о том, где рендерить, где хранить состояние, где выполнять логику, стали архитектурными решениями.
Вторая: пользовательский опыт — это архитектурный компромисс. SPA с богатым интерфейсом и отличной интерактивностью может грузиться 10 секунд. SSG с мгновенным First Paint может быть неудобен для динамических данных. Каждый выбор рендеринга — это компромисс между скоростью, интерактивностью и сложностью.
Третья: фронтенд — это интерфейс всей системы. Пользователю всё равно, как у вас работает ваш высоконагруженный бэкенд с CQRS и Event Sourcing. Если страница грузится 5 секунд, а кнопка дёргается — система для него плохая.
В этой главе мы разберём фронтенд не как «нарисовать красиво», а как архитектурный слой: компоненты, состояния, рендеринг, производительность, тестирование и — самое главное — когда какой подход выбирать.
#Ренессанс SSR: почему серверный рендеринг вернулся
История фронтенда — это история маятника. Сервер рендерит → клиент рендерит → сервер снова рендерит. Каждый шаг — реакция на боли предыдущего.
#Как мы дошли до жизни такой
В начале 2010-х фронтенд выглядел так: сервер генерировал HTML, отдавал его браузеру, а jQuery добавлял немножко интерактивности на уже готовой странице. Рендеринг — серверный (Server-Side Rendering). Быстро, работает, но интерфейсы бедные.
Потом пришли SPA-фреймворки — Angular, React, Vue. Они перетащили рендеринг в браузер. Клиент получал пустой HTML и JavaScript-бандл, который строил весь интерфейс на лету.
Почему SPA победили? Две причины:
- Богатый UX: навигация без перезагрузки страницы, анимации, drag-and-drop, сложные формы. Пользователи полюбили эти интерфейсы — они чувствовались как нативные приложения.
- Разделение команд: фронтенд и бэкенд могли разрабатываться независимо. REST API стал контрактом, и это упростило организацию работы.
Но маятник качнулся слишком далеко. К 2018 году стало очевидно, что SPA — не бесплатный обед:
- Медленный First Paint: пока загрузится бандл, пока выполнится JavaScript, пока React отрендерит DOM — пользователь смотрит на белый экран. На плохом соединении — 5-10 секунд.
- SEO — боль: поисковые боты не выполняют JavaScript. Ваш SPA для них — пустая страница. Приходится извращаться с prerendering.
- Производительность на мобильных: дешёвые Android-смартфоны тратят секунды на парсинг и выполнение JavaScript.
И индустрия сказала: «А давайте снова рендерить на сервере».
#Новый SSR — это не старый PHP
Важно не путать современный SSR с тем, что было в эпоху PHP и JSP. Современный SSR — это гибрид:
- Next.js (React) — дедушка современного SSR. App Router, Server Components, Streaming.
- Nuxt (Vue) — та же идея, но с Vue и модульной экосистемой.
- Remix (React) — минималистичный SSR, построенный вокруг Web API (Fetch, Request, Response).
- SvelteKit (Svelte) — Svelte на сервере с минимальным JavaScript в браузере.
- Astro — максимально статический рендеринг с «островками» интерактивности.
Общая черта всех этих фреймворков — вы не выбираете «или». Вы получаете и то и другое: часть страницы рендерится на сервере, часть — гидратируется на клиенте. Вы решаете, что где рендерить, и это решение — архитектурное.
#Почему это важно архитектурно
Современный SSR переопределяет границы системы. Раньше у вас было: бэкенд отдаёт JSON → фронтенд рендерит UI. Теперь фронтенд-фреймворк может напрямую обращаться к базе данных, кэшу, API на сервере — без отдельного бэкенд-слоя.
В Next.js App Router компонент может быть асинхронным и делать запрос к базе:
// Server Component — выполняется на сервере, никогда не видит браузер
async function UserProfile({ userId }: { userId: string }) {
const user = await db.users.findUnique({ where: { id: userId } });
return <div>{user.name}</div>;
}Этот код работает на сервере. Он не отправляет никакой JavaScript в браузер. Он не раскрывает пароли от базы. Он просто возвращает готовый HTML.
Для архитектора это означает новую размерность решений:
- Что рендерить на сервере? Статический контент, SEO-чувствительные страницы, данные из защищённых источников.
- Что гидратировать на клиенте? Интерактивные элементы, формы, сложные UI-компоненты.
- Где хранить state? Серверные компоненты — stateless. Клиентские — полноценное состояние.
Cross-reference: Подробнее о том, как SSR-фреймворки взаимодействуют с API-слоем — см. Главу 6 «API-архитектура». О масштабировании фронтендов на десятки тысяч страниц и микро-фронтендах — Главу 26 «Архитектура крупных фронтендов».
#Component Architecture: как не построить «компонентный монолит»
Если SSR — это архитектура рендеринга, то компоненты — это архитектура UI. И здесь есть свои паттерны, принципы и — неизбежно — анти-паттерны.
#Atomic Design — архитектура для дизайна
Atomic Design (Брэд Фрост, 2013) — пожалуй, самая влиятельная идея в организации UI-компонентов. Идея простая: UI строится из атомов, как химические молекулы.
- Atoms: базовые элементы — кнопки, инпуты, лейблы, иконки. Неделимые.
- Molecules: комбинации атомов — строка поиска (инпут + кнопка + иконка), карточка товара (изображение + заголовок + цена).
- Organisms: сложные секции — хедер, футер, форма регистрации. Состоят из молекул и атомов.
- Templates: компоновка организмов на странице без реальных данных.
- Pages: шаблоны с реальным контентом — конкретный пример того, как выглядит система.
Atomic Design хорош тем, что даёт язык для общения дизайнеров и разработчиков. Дизайнер рисует атомы в Figma, разработчик реализует те же атомы в коде. Когда оба говорят на одном языке — меньше ошибок на стыке.
Но Atomic Design — не панацея. Практика показала несколько проблем:
Oversplitting: вы начинаете выделять «атомы» там, где это не нужно. Кнопка — атом? Ок. А кнопка с иконкой — это уже молекула? Или всё ещё атом? А если кнопка бывает 12 вариантов — это 12 атомов? Споры на тему «куда положить этот компонент» отнимают больше времени, чем написание самого компонента.
Жёсткая иерархия: реальный мир не всегда укладывается в иерархию «атом → молекула → организм». Иногда карточка товара ведёт себя и как молекула, и как организм. Иногда иконка — самостоятельный компонент, не входящий ни в какую молекулу.
Реальность: Atomic Design — отличная отправная точка. Но в продакшене почти никто не соблюдает эту схему строго. Она становится скорее принципом («клади атомарные вещи отдельно, сложные композиции — отдельно»), чем законом.
#Composition vs Inheritance
В мире React/Vue компоненты общаются через два механизма: наследование (is-a) и композицию (has-a). Современная практика однозначно говорит: композиция побеждает.
// Наследование — неправильно
class PrimaryButton extends Button {
// переопределяем стили
}
// Композиция — правильно
function Button({ variant, children }: { variant: 'primary' | 'secondary', children: ReactNode }) {
return <button className={`btn btn-${variant}`}>{children}</button>;
}Почему композиция? Потому что наследование создаёт жёсткую иерархию, которая ломается при первых изменениях. Хотите кнопку, которая и primary, и с иконкой, и disabled? В мире наследования — ещё один подкласс. В мире композиции — пропсы и слоты.
#Compound Components
Compound Components — паттерн, когда несколько компонентов работают как одно целое, разделяя состояние через контекст.
// Использование
<Select>
<Select.Trigger>Выберите страну</Select.Trigger>
<Select.Options>
<Select.Option value="ru">Россия</Select.Option>
<Select.Option value="us">США</Select.Option>
</Select.Options>
</Select>Здесь Select, Select.Trigger, Select.Options и Select.Option — отдельные компоненты, но они разделяют общее состояние (открыт ли дропдаун, какое значение выбрано) через контекст.
Это мощный паттерн, потому что он даёт гибкость: разработчик может перекомпоновать части в любой последовательности, изменить структуру — и всё продолжит работать. Состояние управляется централизованно, но рендеринг — декларативно.
#Практическое правило архитектуры компонентов
Опыт многих команд сводится к одному принципу: компонент должен уметь ровно одну вещь, но уметь её хорошо.
- Слишком маленький компонент (иконка враппера) — лишняя абстракция
- Слишком большой компонент (форма целиком с валидацией, отправкой, состояниями) — не переиспользуется
Где золотая середина? Тест на трёхкратное повторение. Если один и тот же паттерн использования повторяется трижды — выделите компонент. Если меньше — не надо.
#State Management: архитектура данных на фронтенде
Где живёт состояние? Этот вопрос породил больше религиозных войн, чем выбор между tabs и spaces. Давайте разберёмся трезво.
#Проблема: состояние повсюду
У современного фронтенда — как минимум четыре типа состояния:
- Локальное UI-состояние — открыт ли дропдаун, какая вкладка активна, значение инпута. Живёт в одном компоненте, никому больше не нужно.
- Глобальное UI-состояние — тёмная тема, выбранный язык, sidebar открыт/закрыт. Нужно нескольким компонентам.
- Серверное состояние (Server State) — список пользователей,детали заказа, настройки профиля. Приходит с сервера, его нужно кэшировать, синхронизировать, обновлять.
- URL-состояние — текущий роут, query-параметры, хеш. Живёт в адресной строке.
Ошибка многих фронтенд-архитекторов — сваливать всё в одну кучу. Классика: вы положили список пользователей (серверное состояние) в Redux store (который для глобального UI-состояния). А потом начали синхронизировать: «После мутации вызови dispatch и обнови список в сторе». Забыли — баг.
#Server State ≠ Client State
Это, пожалуй, самый важный урок state management за последние пять лет.
Client State — локальное, живёт в браузере, контролируется фронтендом. Открыт попап, выбран язык, текущий шаг в многошаговой форме.
Server State — приходит с сервера, живёт на сервере, фронтенд — только кэш. Список пользователей, заказ, баланс счёта.
Проблема в том, что server state:
- Может измениться на сервере без вашего ведома (другой пользователь обновил данные)
- Имеет собственный lifecycle (загрузка, ошибка, stale, revalidation)
- Требует синхронизации (refetch, optimistic updates, cache invalidation)
Грабли, на которые наступают все: класть server state в глобальный Redux/Zustand store и пытаться синхронизировать его руками.
Решение появилось в виде библиотек для server state: React Query (TanStack Query), SWR, RTK Query (часть Redux Toolkit), Apollo Client (для GraphQL).
// React Query — server state management
function UsersPage() {
const { data, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(r => r.json()),
staleTime: 5 * 60 * 1000, // 5 минут до stale
});
if (isLoading) return <Spinner />;
if (error) return <Error />;
return <UserList users={data} />;
}Обратите внимание: нет никакого dispatch, никакого store, никакой ручной синхронизации. React Query сам управляет кэшем, сам решает, когда обновить данные, сам обрабатывает состояния загрузки и ошибки. И — что важно — кэш автоматически инвалидируется после мутации.
// Мутация — автоматическая инвалидация кэша
function useCreateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (user) => fetch('/api/users', { method: 'POST', body: JSON.stringify(user) }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
}В этом дизайне фронтенд — это слой представления, кэширующий данные с сервера. Он не пытается быть источником правды (source of truth). Источник правды — сервер. Фронтенд — кэш.
#Когда всё-таки нужен глобальный store
Global state management (Redux, Zustand, Pinia, MobX) — для UI-состояния, которое нужно в разных частях приложения. Реальные примеры:
- Тема оформления — тёмная/светлая
- Выбранный язык — i18n
- Корзина — если это интернет-магазин (но и то: можно хранить на сервере)
- Состояние аутентификации — текущий пользователь (гибрид server+client)
- Сложная многошаговая форма — Wizard с шагами
Всё остальное — либо локальное состояние компонента (useState), либо server state (React Query).
Хороший вопрос на собеседовании: «Когда вам нужен Redux?» Хороший ответ: «Когда есть сложное состояние, которое нужно большому числу независимых компонентов в разных частях приложения. Для server state — не нужен, там есть TanStack Query».
#URL как состояние
Есть ещё одна категория состояния, которую часто забывают — URL. Query-параметры, хеш, роут.
/users?page=2&sort=name&filter=activeЭто не просто «адрес страницы». Это полноценное состояние приложения: текущая страница пагинации, сортировка, фильтр. И его лучше хранить в URL, а не в React-состоянии. Почему?
- Пользователь может поделиться ссылкой — «посмотри на второй странице с сортировкой по имени»
- Работает кнопка «Назад» — переход в предыдущее состояние браузера
- Не теряется при перезагрузке — в отличие от React-состояния
Правило: если состояние определяет, что пользователь видит на странице — оно должно быть в URL. Если состояние определяет, как это выглядит (анимация, открытый дропдаун) — оно должно быть в компоненте.
#Rendering Patterns: понимание компромиссов
Мы уже обсудили ренессанс SSR, но давайте расставим все точки над i. Вот полная карта рендеринг-паттернов, с которыми вы столкнётесь:
#CSR (Client-Side Rendering)
SPA в чистом виде. Сервер отдаёт пустой HTML, браузер загружает JavaScript-бандл, рендерит UI.
Плюсы: богатая интерактивность, один бандл на весь сайт, разделение с бэкендом. Минусы: медленный First Paint, плохой SEO, тяжёлый для мобильных.
Когда: панели управления (dashboards), SaaS-интерфейсы за логином, приложения, где SEO не важен.
#SSR (Server-Side Rendering)
Сервер рендерит React/Vue в HTML, отдаёт готовую страницу. После загрузки — гидратация (hydration), подключение JavaScript к уже готовому DOM.
Плюсы: быстрый First Paint, SEO, работает без JS (первичный контент). Минусы: нагрузка на сервер, гидратация может дёргать UI, TTFB выше (сервер считает).
Когда: контентные сайты, e-commerce, блоги — где SEO и скорость первого показа важны.
#SSG (Static Site Generation)
Страницы генерируются на этапе сборки. В продакшене — статические HTML-файлы на CDN.
Плюсы: максимальная скорость, минимальные затраты на сервер, CDN из коробки. Минусы: контент устаревает до следующей сборки. Не для динамических данных.
Когда: документация, блоги, маркетинговые страницы, сайты с редкими обновлениями.
#ISR (Incremental Static Regeneration)
Как SSG, но страницы перегенерируются по требованию при обращении пользователя. После первой загрузки — статика. При следующем обращении — проверка, не устарела ли.
Плюсы: скорость SSG + свежесть контента. Минусы: сложность инвалидации, stale данные до следующей регенерации.
Когда: e-commerce (страницы товаров, цены меняются не каждую минуту), новостные порталы.
#Streaming SSR
Сервер рендерит страницу по частям — отправляет HTML порциями, не дожидаясь полного рендеринга. Пользователь видит контент постепенно, не дожидаясь самой медленной части.
Плюсы: очень быстрый первый контент (почти мгновенно), постепенное улучшение. Минусы: сложно настраивать, не все фреймворки поддерживают.
Когда: контентные страницы с тяжёлыми секциями (медленные запросы к БД, большие изображения).
#Islands Architecture (Astro)
Минимум JavaScript: страница — статический HTML, а интерактивные элементы — изолированные «островки» с собственным JavaScript.
// Astro — всё статическое, кроме интерактивных элементов
---
import Header from '../components/Header.astro';
import LikeButton from '../components/LikeButton.jsx';
---
<Header /> {/* статический HTML, ноль JS */}
<LikeButton client:load /> {/* интерактивный островок */}
<p>Этот текст — обычный HTML</p>Плюсы: минимальный JavaScript, отличная производительность, только нужный JS. Минусы: островки не общаются друг с другом, ограниченная интерактивность.
Когда: контентные сайты с несколькими интерактивными элементами (блоги с лайками, формы подписки).
#Decision Matrix
| Критерий | CSR | SSR | SSG | ISR | Streaming SSR | Islands |
|---|---|---|---|---|---|---|
| First Paint (FCP) | Плохой | Отличный | Отличный | Отличный | Отличный | Отличный |
| Time to Interactive | Плохой | Хороший | Отличный | Отличный | Отличный | Отличный |
| SEO | Плохой | Отличный | Отличный | Отличный | Отличный | Отличный |
| Динамические данные | Отлично | Хорошо | Плохо | Хорошо | Хорошо | Хорошо |
| Интерактивность | Отлично | Средне | Средне | Средне | Средне | Средне |
| Server cost | Низкий | Средний | Низкий | Средний | Средний | Низкий |
| Complexity | Низкая | Средняя | Низкая | Средняя | Высокая | Средняя |
Главный вопрос при выборе: «Как пользователь взаимодействует с этим экраном?». Если просматривает — стремитесь к Static/SSR. Если активно кликает и взаимодействует — возможно, CSR с умным лоадингом.
#Performance: от Lighthouse до Real User Monitoring
Производительность фронтенда — это не «у нас метрики в зелёной зоне». Это деньги. Amazon подсчитал: 100ms задержки = 1% падения продаж. Google: 500ms = 20% падения трафика. Это не абстрактные цифры.
#Core Web Vitals — то, что реально измеряет Google
В 2020 году Google объявил, что Core Web Vitals влияют на ранжирование в поиске. С тех пор это стандарт де-факто для измерения производительности фронтенда:
- LCP (Largest Contentful Paint): как быстро загружается основной контент. Хорошо: < 2.5s.
- FID (First Input Delay) / INP (Interaction to Next Paint): как быстро реагирует на первое взаимодействие. Хорошо: < 100ms. В 2024 INP заменил FID.
- CLS (Cumulative Layout Shift): насколько «дёргается» страница при загрузке. Хорошо: < 0.1.
Измерять их можно по-разному:
Lab data (лабораторные данные): Lighthouse, PageSpeed Insights. Дают конкретные цифры, воспроизводимы. Но — симулируют конкретное устройство, конкретное соединение. Не отражают реального опыта пользователей.
RUM (Real User Monitoring): сбор метрик с реальных браузеров пользователей. Инструменты — Google Analytics Core Web Vitals, Sentry Performance, Datadog RUM, New Relic Browser.
Lab data: "На MacBook Pro с Fast 3G страница грузится 2.1s"
RUM: "На Samsung Galaxy A10 с 3G в Индонезии — 8.4s"История из жизни: вы проверяете производительность на Lighthouse — всё зелёное. А пользователи жалуются, что «всё тормозит». Почему? Потому что у них не MacBook Pro, а дешёвый китайский телефон и Edge на Windows 10. RUM покажет реальную картину. Lighthouse — только приближение.
#Архитектурные решения для производительности
Производительность фронтенда — это не «давайте сожмём изображения». Это архитектурные решения:
- CDN для статики — CSS, JS, изображения с Fastly/CloudFlare/Akamai. Базовый уровень.
- Code Splitting — не грузить бандл целиком, а разбить на чанки по роутам. React.lazy + Suspense.
- Preloading critical resources —
<link rel="preload" href="critical.css" as="style"> - Optimistic UI — показать результат до ответа сервера, обновить при ошибке.
- Bundle size budget — «Если бандл > 200KB gzip — CI падает».
- Image optimization — responsive images (srcset), AVIF/WebP, lazy loading.
- Font loading strategy — font-display: swap, subsetting, не блокировать рендеринг.
- Critical CSS — инлайн только critical CSS в
<head>, остальное подгружать асинхронно.
#Lighthouse — не цель, а инструмент
Многие команды делают ошибку: оптимизируют под Lighthouse. Ставят цель «Lighthouse 100». Добавляют кучу костылей, которые улучшают скор, но не улучшают пользовательский опыт.
Lighthouse — это суррогатная метрика. Она полезна для поиска проблем, но не должна быть целью. Реальная цель — субъективная скорость для пользователя. Сколько времени проходит от клика до того, как пользователь может сделать то, что хотел?
#Bundle Size Budgeting: защита от ожирения
Одна из самых недооценённых архитектурных практик во фронтенде — bundle budget. Идея проста: вы устанавливаете лимит на размер бандла в CI, и сборка падает, если лимит превышен.
// Пример бюджета (webpack.config.js)
{
budget: [
{ type: 'initial', maxSize: '170KB' }, // initial JS — то, что грузится сразу
{ type: 'async', maxSize: '100KB' }, // lazy-loaded chunks
{ type: 'asset', maxSize: '50KB' }, // изображения, шрифты
]
}Зачем? Потому что bundle size — это архитектурный компромисс, который незаметно ухудшается, если его не контролировать.
«Мы просто подключили moment.js для форматирования даты» → +50KB. «И lodash целиком, tree-shaking же есть» → на деле ломается. «Иконки из Font Awesome — все, а пофиг» → +200KB.
Через год вы имеете 3MB бандла и не понимаете, почему сайт тормозит. Bundle budget автоматически ловит такие проблемы.
Практика: Настройте визуализацию бандла (webpack-bundle-analyzer, esbuild-visualizer) и смотрите, что занимает место, перед каждым релизом. Вы удивитесь, сколько весят библиотеки, которые «ну буквально пара функций».
#Frontend Testing Strategy: пирамида не врёт
Тестирование фронтенда — больное место. Много шума, мало понимания, что на самом деле тестировать.
#Пирамида тестирования (она же не торт)
╱ E2E ╲ — несколько сценариев, полный browser
╱─────────╲
╱ Integration ╲ — компонент + реальный API / БД
╱───────────────╲
╱ Unit Tests ╲ — изолированные функции и компоненты
╱─────────────────────╲Проблема фронтенд-тестирования — в перевёрнутой пирамиде. Команды пишут тонны E2E-тестов (Cypress, Playwright) и почти не пишут unit-тесты. Результат: E2E-тесты хрупкие (малейшее изменение DOM — упало), медленные (минуты против секунд), и нелокализуют проблему (тест упал — где?).
Архитектурно правильный подход:
Unit tests (60-70%): Тестируйте изолированные функции — утилиты, хуки, редюсеры, преобразование данных. Не тестируйте «компонент рендерится» — это ложно позитивно.
// Полезный unit test
test('formatPrice formats cents to dollars', () => {
expect(formatPrice(1000)).toBe('$10.00');
expect(formatPrice(0)).toBe('$0.00');
});
// Бесполезный unit test
test('Button renders with text', () => {
render(<Button>Click</Button>);
expect(screen.getByText('Click')).toBeInTheDocument();
// Да, кнопка рендерится. Ну и что?
});Integration tests (20-30%): Тестируйте поведение компонента с реальными данными и API. React Testing Library, MSW (Mock Service Worker) для перехвата запросов. Это даёт наибольшую отдачу — вы тестируете то, что видит пользователь.
// Integration test — пользовательский сценарий
test('User can search and see results', async () => {
render(<SearchPage />);
await userEvent.type(screen.getByPlaceholderText('Search...'), 'pizza');
await userEvent.click(screen.getByRole('button', { name: 'Search' }));
await waitFor(() => {
expect(screen.getByText('Margherita')).toBeInTheDocument();
});
});E2E tests (5-10%): Критические сценарии — логин, покупка, регистрация. Playwright или Cypress в headless browser. Не пишите E2E для каждой фичи — только для того, что сломает бизнес, если не работает.
Visual tests (10%): Скриншотные тесты (Percy, Chromatic) сравнивают визуальные изменения. Спасают от регрессий, когда изменение CSS в одном месте ломает другое. Важный нюанс: визуальные тесты дают много ложных срабатываний (каждый пиксель считается). Настройте толерантность.
#Реальность: что на самом деле нужно
Большинству проектов достаточно:
- Тестировать data flow (unit + integration)
- Тестировать критические сценарии (E2E)
- Тестировать визуальную регрессию (visual)
Не нужно:
- Покрытие 100% — это дорого и не даёт пользы
- Снэпшот-тесты — они ломаются от любого пробела
- E2E для каждого экрана — вы утонете в флакующих тестах
#Micro-frontends: когда лечить сложность ещё большей сложностью
Micro-frontends — это попытка применить принципы микросервисов к фронтенду. Идея: разные команды владеют разными частями UI, независимо разрабатывают и деплоят их.
#Как это работает
Существует несколько подходов к интеграции микро-фронтендов:
Webpack Module Federation (самый популярный): основное приложение динамически загружает модули из других приложений, как npm-пакеты, только в runtime.
// Host приложение загружает remote-компонент
const ProductPage = React.lazy(() => import('catalog/ProductPage'));
const Checkout = React.lazy(() => import('checkout/Checkout'));Podium — более архитектурный подход от Finn. Каждый микро-фронтенд — это изолированное приложение со своим сервером. Основной «shell» собирает фрагменты HTML с разных серверов и склеивает на серверной стороне.
Web Components — интеграция через Custom Elements. Каждый микро-фронтенд компилируется в Web Component — независимо от фреймворка.
#Когда микро-фронтенды — правильное решение
- Несколько команд, каждая владеет своей частью приложения
- Разные темпы релизов — одна команда деплоит раз в день, другая — раз в месяц
- Разные технологии — одна команда на React, другая на Vue (редко, но бывает)
- Очень большой фронтенд — сотни страниц, десятки тысяч компонентов
#Когда микро-фронтенды — ошибка
Большинство проектов не попадают в эту категорию. Вот когда не надо:
Команда 2-5 человек. У вас нет проблемы, которую решают микро-фронтенды. У вас — проблема организации кода внутри одного приложения. Решение — модульный монолит на фронтенде, а не микро-фронтенды.
Нет зрелости в микросервисах. Если ваш бэкенд — монолит с распределёнными костылями, не смейтесь: микро-фронтенды без микро-бэкенда — это архитектурный оксюморон.
Не готовы платить за сложность. Микро-фронтенды — это:
- Дублирование зависимостей (каждый микро-фронтенд несёт свой React)
- Performance overhead (runtime загрузка модулей)
- Сложность интеграционного тестирования
- Shared dependencies management — версионирование
- Сложная навигация между микро-фронтендами
Cross-reference: Если у вас действительно большая кодовая база, требующая микро-фронтендов — см. Главу 26 «Архитектура крупных фронтендов». Там вопрос раскрыт глубже: монорепозитории, масштабирование фиче-флагов, A/B-тестирование на фронтенде.
#Design Systems: строить или покупать
Design System — это единый набор компонентов, токенов (цвета, отступы, типографика) и гайдлайнов для всего приложения. Когда-то это называлось «UI Kit», но теперь стало серьёзной частью фронтенд-архитектуры.
#Когда строить свои компоненты
Плюсы: полный контроль, без legacy зависимостей, уникальный дизайн. Минусы: нужно поддерживать — accessibility, версионирование, тестирование, документацию, тему.
Строить свои имеет смысл, когда:
- У вас уникальный дизайн (не Material Design, не Ant Design)
- У вас есть дизайнеры, которые постоянно добавляют новые компоненты
- У вас несколько продуктов, которые используют один базовый дизайн
- У вас > 10 фронтенд-разработчиков
#Когда использовать библиотеку (Material UI, Ant Design, Radix, shadcn/ui)
Плюсы: батарейки включены — accessibility из коробки, проверено тысячами проектов, обновляется сообществом. Минусы: приложение выглядит как типовое, сложно кастомизировать, версионирование — чужая воля.
Использовать библиотеку — когда:
- У вас нет дизайнера или дизайн простой
- Вы хотите быстро запустить MVP
- У вас маленькая команда (1-5 человек)
- Дизайн — не ваше конкурентное преимущество
#Современные подходы: headless UI + design tokens
Headless UI (Radix, Headless UI, React Aria) — библиотека, которая даёт поведение (accessibility, keyboard navigation, focus management) без встроенных стилей. Вы сами рисуете, как хотите.
// Radix UI — поведение отдельно, стили отдельно
import * as Dialog from '@radix-ui/react-dialog';
function MyDialog() {
return (
<Dialog.Root>
<Dialog.Trigger>Open</Dialog.Trigger>
<Dialog.Content>
<Dialog.Title>My dialog</Dialog.Title>
<Dialog.Description>Content</Dialog.Description>
<Dialog.Close>Close</Dialog.Close>
</Dialog.Content>
</Dialog.Root>
);
}Вы получаете правильное поведение из коробки (фокус, Escape, ARIA-атрибуты), а стилизуете через CSS — свои токены, свою тему. Это золотая середина: не изобретать велосипед accessibility, но не быть заложником чужого дизайна.
#Компромиссы и альтернативы
#Rendering Pattern: SPA vs SSR — главный компромисс книги
| Выбираете | Получаете | Платите |
|---|---|---|
| SPA (CSR) | Богатый UX, независимые команды | Медленный First Paint, SEO проблемы |
| SSR | Быстрый First Paint, SEO | Нагрузка на сервер, сложность гидратации |
| SSG | Максимальная скорость, CDN | Stale data, нет динамики |
| ISR | Компромисс между SSG и SSR | Сложность инвалидации |
| Streaming SSR | Быстрый First Paint, поэтапный рендеринг | Сложность, не все фреймворки |
#State Management: server state библиотека vs Redux
| Выбираете | Получаете | Платите |
|---|---|---|
| React Query / SWR | Автоматический кэш, инвалидация, stale-while-revalidate | Только для server state |
| Redux / Zustand | Полный контроль над состоянием | Бойлерплейт, ручная синхронизация |
| URL как state | Sharing, back button, refresh | Только для навигационного состояния |
#Micro-frontends vs Monolithic Frontend
| Выбираете | Получаете | Платите |
|---|---|---|
| Monolithic frontend | Простота, одна кодовая база | Разрастается, одна команда |
| Micro-frontends | Независимые команды и деплои | Performance cost, duplication, ops complexity |
#Build vs Buy: Design System
| Выбираете | Получаете | Платите |
|---|---|---|
| Свои компоненты | Уникальный дизайн, полный контроль | Дорого поддерживать |
| Библиотека (MUI, Ant) | Готово, accessibility из коробки | Типовой вид, сложная кастомизация |
| Headless UI (Radix) | Поведение из коробки, свои стили | Меньше готовых компонентов |
#Когда это применимо
#Для стартапа (1-5 разработчиков)
- CSR с умным лоадингом — не надо усложнять SSR, если SEO не важен
- React Query для server state — минимум бойлерплейта
- Готовая библиотека (MUI, Ant) — не изобретайте дизайн-систему
- Никаких микро-фронтендов — категорически
- Bundle budget — сразу, это одна строчка конфига
#Для средней компании (5-20 разработчиков)
- Next.js / Nuxt — SSR или ISR для контентных страниц
- Atomic Design — как ориентир, не как закон
- Custom design system — если есть дизайнер и >5 разработчиков
- RUM — настройте сразу, это окупится на первом инциденте
- Visual regression testing — Chromatic, Percy
- Core Web Vitals в CI — проваливать сборку, если метрики упали
#Для enterprise (20+ разработчиков)
- Micro-frontends — только если доказана необходимость на уровне организации
- Module Federation или Podium для интеграции
- Монорепозиторий (Nx, Turborepo) для управления кодовой базой
- Design System как отдельный пакет — versioned, documented, tested
- Production-grade RUM — Datadog, New Relic, Sentry
- Feature flags — LaunchDarkly, Unleash для безопасных релизов
#Кейс: Как эволюционировала архитектура фронтенда в DoorDash
DoorDash — классический пример того, как фронтенд-архитектура росла вместе с бизнесом. В 2013 году это был простой сайт доставки. В 2024 — платформа на миллионы заказов в день.
#Фаза 1: Быстрый старт (2013-2015)
На старте DoorDash использовал монолитный Django-фронтенд. Сервер рендерил HTML с помощью шаблонов Django. Никакого React, никаких SPA. Просто, быстро, работает.
Архитектура: серверное Django-приложение с шаблонами.
Почему: скорость запуска. У команды не было времени на «красивый фронтенд». Нужно было проверить гипотезу рынка.
#Фаза 2: SPA и React (2015-2018)
По мере роста продукта возникла потребность в более богатом UX:
- Реальное время — отслеживание курьера на карте
- Drag-and-drop для кастомизации заказа
- Мгновенная обратная связь при выборе ресторана
DoorDash перешёл на Single Page Application на React. Django стал API-сервером, фронтенд — отдельным приложением. Это дало богатый UX, который был нужен продукту.
Архитектура: React SPA + Django REST API.
Боль: bundle size. React SPA весил всё больше, страницы грузились медленно на мобильных. DoorDash заметил, что конверсия падает с каждой секундой загрузки.
#Фаза 3: Server-Side Rendering (2018-2020)
DoorDash внедрил Server-Side Rendering на React. Критические страницы (главная, поиск ресторанов) рендерились на сервере, остальные — как SPA. Это улучшило First Paint и SEO без потери интерактивности.
Архитектура: React SSR для критических страниц + React SPA для внутренних страниц (дашборд курьера, история заказов).
Решение: гибридный подход, где тип рендеринга зависел от контекста страницы.
#Фаза 4: Micro-frontends + Performance (2020-present)
С ростом до глобального масштаба DoorDash столкнулся с тем, что один фронтенд-репозиторий не масштабируется. Разные команды (consumer, dasher, merchant) имели разные темпы релизов и разные требования.
DoorDash внедрил Webpack Module Federation для разделения фронтенда на микро-фронтенды:
- Consumer app — то, что видят пользователи (заказ еды)
- Dasher app — дашборд курьера (приём заказов, навигация)
- Merchant portal — портал ресторанов (меню, заказы, аналитика)
Каждый микро-фронтенд — отдельный репозиторий, отдельный CI/CD, отдельная команда.
Архитектура: Module Federation + Custom Shell (микро-фронтенды) + SSR для контентных страниц.
#Архитектурные уроки DoorDash
- Начинали с простого. Django templates — не стыдно, когда ищешь product-market fit.
- SSR как улучшение, не замена. DoorDash не отказался от SPA, а добавил SSR для критических страниц.
- Micro-frontends — только когда стало больно. Не на старте, не на этапе роста — а когда одна кодовая база явно тормозила развитие.
- Performance — это конверсия. DoorDash измерял влияние каждого архитектурного решения на бизнес-метрики, а не только на lighthouse scores.
- Гибрид — это норма. Не все страницы должны быть SSR, не все — SPA. Разный контент — разный рендеринг.
[Anecdotal] Я видел похожую эволюцию в других компаниях. Путь «templates → SPA → SSR → гибрид» — почти универсальный для успешных продуктов. Вопрос не в том, какой подход «правильный», а на каком этапе вы находитесь.
#Чек-лист для самопроверки
- Rendering pattern выбран осознанно: вы знаете, почему CSR, а не SSR (или наоборот), и какие компромиссы вы приняли
- Server state отделён от client state: TanStack Query / SWR для серверных данных, локальный state / URL для клиентских
- Bundle budget настроен в CI: сборка падает, если бандл превышает лимит
- Code splitting работает: lazy loading по роутам, критические ресурсы загружаются раньше
- Core Web Vitals измеряются в RUM: вы видите реальную производительность у пользователей, не только Lighthouse
- Design System или компонентная библиотека: вы не пишете кнопку с нуля в 15-й раз
- Тестовая стратегия определена: unit > integration > E2E > visual, а не «напишем E2E на всё»
- Изображения оптимизированы: responsive images, современные форматы, lazy loading
- Микро-фронтенды — только если доказана необходимость: вы не используете их, потому что «это модно»
- Производительность — архитектурное решение, не героический фикс: вы проектируете для перфы, не фиксите её после релиза
#«Не делай так»: анти-паттерны фронтенд-архитектуры
#1. Over-engineering state management
Симптом: Redux store на 500 строк для приложения из трёх страниц. Actions, reducers, selectors, middleware — вся папочка store/ для двух API-запросов.
Почему это плохо: вы платите complexity budget за то, что не нужно. Каждый новый разработчик тратит час на понимание, почему дропдаун открывается через Redux.
Как надо: useState() — достаточно. Контекст — для простого shared state. TanStack Query — для server state. Redux — только когда доказана необходимость (сложный global state с middleware).
#2. Micro-frontends для трёх разработчиков
Симптом: команда из двух фронтендеров и одного бэкендера обсуждает, как настроить Module Federation. Через месяц — первый микро-фронтенд работает. Через три — никто не помнит, где лежит shared компонент.
Почему это плохо: вы решили проблему, которой у вас нет. Микро-фронтенды решают организационную проблему (несколько команд, разные темпы релизов). Если у вас одна команда — у вас нет этой проблемы. У вас — проблема организации кода.
Как надо: монолитный фронтенд + модульная организация кода. А если хочется красиво — Nx/Turborepo с явно выделенными библиотеками.
#3. Игнорирование bundle size
Симптом: «У нас 5MB бандла? Ну, интернеты сейчас быстрые». Или классика: moment.js как зависимость (с локалями всех стран мира, которые никогда не используются).
Почему это плохо: 5MB на десктопе — 2 секунды. 5MB на мобильном 3G в метро — 15 секунд. Пользователь уходит. Навсегда.
Как надо: bundle budget в CI + bundle analyzer. Каждая новая зависимость должна пройти review: «А точно ли нам нужны все 50KB этой библиотеки, или можно 2KB своей функции?»
#4. SPA на каждый чих (SPA for everything)
Симптом: сайт-визитка на React, который грузит 1.5MB для того, чтобы показать «О компании» и «Контакты».
Почему это плохо: вы тратите ресурсы пользователя (трафик, батарею, процессор) на то, что можно отдать статическим HTML.
Как надо: статический HTML (Astro, Eleventy) для контентных страниц, SPA — только когда нужна сложная интерактивность. Next.js/Nuxt — для гибрида.
#5. Тестирование не того
Симптом: 200 E2E-тестов, 0 unit-тестов, 5 integration-тестов. E2E падают при любом изменении CSS-класса. Команда тратит понедельник на фикс тестов.
Почему это плохо: вы тестируете реализацию (DOM), а не поведение. И это дорого, медленно и хрупко.
Как надо: unit-тесты для логики, integration-тесты для сценариев пользователя (с MSW), E2E — только для критического пути. Пирамида, не торт.
#6. «Наш дизайн — уникальный» (и своя дизайн-система из 3 компонентов)
Симптом: команда из трёх разработчиков пишет свою дизайн-систему, потому что «Material UI выглядит не так». Через месяц — кнопка есть, а dropdown — нет, accessibility сломана, документация не написана.
Почему это плохо: вы сделали то, что уже сделано за вас. Ваше конкурентное преимущество — не CSS-свойства кнопки. Ваше преимущество — бизнес-логика.
Как надо: используйте готовую библиотеку (Radix, shadcn/ui, MUI) + дизайн-токены. Кастомизируйте через тему, не переписывая компонент с нуля.
#Ключевые выводы
-
Фронтенд — это архитектура, а не «нарисовать красиво». Выбор рендеринга, управление состоянием, организация компонентов — архитектурные решения с реальными компромиссами.
-
Server state ≠ client state. Серверное состояние — не Redux/Zustand, а TanStack Query/SWR. Клиентское состояние — useState, Context, или Redux для сложных случаев. Не смейте их путать.
-
Ренессанс SSR — не мода, а исправление перекоса. SPA хороши для богатого UX. SSR/SSG — для скорости и доступности. Современные фреймворки дают и то и другое, но вы должны решать, что где.
-
Bundle budget — это закон, а не пожелание. Если не контролировать размер бандла, он вырастет до размера, который убьёт производительность. Настройте автоматическую защиту.
-
Микро-фронтенды — для организации, не для технологии. Они решают проблему нескольких команд с разными темпами релизов. Для одной команды — модульный монолит внутри одного приложения.
-
Производительность — архитектурное решение. Не героический фикс перед релизом, а интегральная часть каждого архитектурного решения — от выбора рендеринга до дизайн-системы.
Forward reference: Как AI меняет проектирование UI — см. Главу 33 «Промпт-инжиниринг как архитектурная дисциплина». Дизайн-системы и компонентная архитектура фронтенда напрямую связаны с тем, как AI-assisted генерация интерфейсов меняет работу архитектора.