Часть II: Строительные блоки
Глава 8

Фронтенд-архитектура — не просто «как красиво нарисовать»

Проблема

Проблема: Фронтенд больше не «прослойка между бэкендом и пользователем». Это самостоятельная система со своей архитектурой, которая может быть источником как преимуществ, так и катастрофических проблем. И да, это книга в первую очередь серверная, но игнорировать фронтенд-архитектуру в книге о веб-системах — всё равно что писать книгу об автомобилях и не упомянуть колёса.


#Введение: почему фронтенд — это архитектура

Лет пятнадцать назад фронтенд был простым. Отправил 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

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. Давайте разберёмся трезво.

#Проблема: состояние повсюду

У современного фронтенда — как минимум четыре типа состояния:

  1. Локальное UI-состояние — открыт ли дропдаун, какая вкладка активна, значение инпута. Живёт в одном компоненте, никому больше не нужно.
  2. Глобальное UI-состояние — тёмная тема, выбранный язык, sidebar открыт/закрыт. Нужно нескольким компонентам.
  3. Серверное состояние (Server State) — список пользователей,детали заказа, настройки профиля. Приходит с сервера, его нужно кэшировать, синхронизировать, обновлять.
  4. 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

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

  1. Начинали с простого. Django templates — не стыдно, когда ищешь product-market fit.
  2. SSR как улучшение, не замена. DoorDash не отказался от SPA, а добавил SSR для критических страниц.
  3. Micro-frontends — только когда стало больно. Не на старте, не на этапе роста — а когда одна кодовая база явно тормозила развитие.
  4. Performance — это конверсия. DoorDash измерял влияние каждого архитектурного решения на бизнес-метрики, а не только на lighthouse scores.
  5. Гибрид — это норма. Не все страницы должны быть SSR, не все — SPA. Разный контент — разный рендеринг.
Anecdotal

[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) + дизайн-токены. Кастомизируйте через тему, не переписывая компонент с нуля.


#Ключевые выводы

  1. Фронтенд — это архитектура, а не «нарисовать красиво». Выбор рендеринга, управление состоянием, организация компонентов — архитектурные решения с реальными компромиссами.

  2. Server state ≠ client state. Серверное состояние — не Redux/Zustand, а TanStack Query/SWR. Клиентское состояние — useState, Context, или Redux для сложных случаев. Не смейте их путать.

  3. Ренессанс SSR — не мода, а исправление перекоса. SPA хороши для богатого UX. SSR/SSG — для скорости и доступности. Современные фреймворки дают и то и другое, но вы должны решать, что где.

  4. Bundle budget — это закон, а не пожелание. Если не контролировать размер бандла, он вырастет до размера, который убьёт производительность. Настройте автоматическую защиту.

  5. Микро-фронтенды — для организации, не для технологии. Они решают проблему нескольких команд с разными темпами релизов. Для одной команды — модульный монолит внутри одного приложения.

  6. Производительность — архитектурное решение. Не героический фикс перед релизом, а интегральная часть каждого архитектурного решения — от выбора рендеринга до дизайн-системы.

Forward reference: Как AI меняет проектирование UI — см. Главу 33 «Промпт-инжиниринг как архитектурная дисциплина». Дизайн-системы и компонентная архитектура фронтенда напрямую связаны с тем, как AI-assisted генерация интерфейсов меняет работу архитектора.