Архитектура крупных фронтендов — когда фронтенд — это не «просто скрипты»
Проблема: Фронтенд перестал быть прослойкой между бэкендом и браузером. Когда кодовая база переваливает за миллион строк, команда — за 50 человек, а количество страниц — за тысячу, фронтенд превращается в самостоятельную распределённую систему. Со своими законами, паттернами и — неизбежно — проблемами.
#Введение: добро пожаловать в мир, где фронтенд — это боль
В Главе 8 мы разобрали фронтенд-архитектуру в целом: компоненты, рендеринг, состояние, тестирование. Это был обязательный минимум. Но вот вопрос: что происходит, когда ваш фронтенд перерастает стадию «один продукт — одна команда — один репозиторий»?
Ответ: начинаются проблемы, которых нет в учебниках по React.
Я работал над проектом, где фронтенд-кодовая база весила 2.5 миллиона строк TypeScript. Однажды мы попытались собрать production-бандл — webpack упал с OutOfMemoryError. Пришлось выделять билд-серверу 32GB RAM. Это был момент истины: фронтенд перестал быть «просто скриптами» и стал инфраструктурной проблемой.
Но размер кода — только вершина айсберга. Реальные проблемы начинаются там, где код пересекается с людьми:
- 10 команд работают над одним фронтендом — как не переехать друг другу ноги?
- Одна команда хочет React 18, другая — Vue 3, третья только что переоткрыла Svelte
- Дизайн-система меняется — как обновить 2000 компонентов без слёз?
- Нужно запустить A/B-тест на 10% пользователей — архитектура должна это выдержать
- Кто-то случайно задеплоил сломанный CSS — и 5 миллионов пользователей видят «синий экран смерти» (буквально)
Добро пожаловать в мир крупных фронтендов. Здесь свои законы, свои инструменты и свои архитектурные компромиссы.
Cross-reference: Эта глава опирается на фундамент, заложенный в Главе 8 «Фронтенд-архитектура». Если вы ещё не читали её — хотя бы пробегитесь по секциям про Component Architecture и Rendering Patterns. Здесь мы предполагаем, что база у вас уже есть, и идём глубже — в масштабирование на уровне организации.
#Monorepo vs Polyrepo: религия разделения
Первое, что вы упрётесь в попытке масштабировать фронтенд — организация кода. И здесь есть два лагеря, готовых драться насмерть.
#Polyrepo: каждая команда — отдельный проект
Polyrepo — это когда каждая команда (или каждый продукт) имеет свой репозиторий. Фронтенд админки лежит в одном репозитории, публичный сайт — в другом, компоненты дизайн-системы — в третьем.
Когда это работает:
- Команды полностью независимы, релизятся в разном ритме
- Нет общего кода (или он минимальный)
- Каждая команда может выбирать свой стек
Когда это ломается:
- Обновление дизайн-системы становится квестом: создаёшь PR в 15 репозиториев, ждёшь 15 код-ревью, молишься, чтобы нигде не сломалось
- Дублирование конфигов, CI-пайплайнов, линтеров и типов — в каждом репозитории свой
.eslintrc, свойtsconfig, свой «не забудь указать node version» - Переиспользование кода — через npm-пакеты, что значит: новая версия → публикация → обновление во всех проектах → чей-то Monday morning
- «А где лежит эта утилита?» — поиск по 20 репозиториям
Реальность: polyrepo работает для компаний, где продукты действительно независимы (например, разные линейки в enterprise). Но для основной массы проектов polyrepo — это способ сделать общий код максимально недоступным.
#Monorepo: один репозиторий, чтобы править всеми
Monorepo — это когда весь код (или хотя бы весь фронтенд) лежит в одном репозитории. Google, Meta, Microsoft, Twitter — все крупные технологические компании используют monorepo для фронтенда (и не только).
Когда это спасает:
- Обновление общей библиотеки — один PR на весь код. Изменил
Button— изменилось всё, тесты проверили, можно деплоить - Единые линтер, tsconfig, CI-конфиги — все на одной версии
- Рефакторинг через всю кодовую базу — IDE и TypeScript справляются, если инструменты настроены
- Поиск кода — grep по миллиону строк работает, если не тупить с regex
Когда это убивает:
- Билд занимает час. Потому что каждый коммит пересобирает весь проект
- CI падает по вине команды, которая ничего не трогала — «опять кто-то сломал тесты в админке, а мой PR к checkout не связан»
- Права доступа — «как дать доступ к одному пакету, но не ко всему репозиторию?»
- История git — 200 тысяч коммитов,
git logгрузится 5 секунд
Хорошая новость: для monorepo есть инструменты, которые решают эти проблемы (почти полностью). Плохая новость: они требуют правильной настройки.
#Инструменты monorepo для фронтенда
Nx — пожалуй, самый зрелый инструмент для фронтенд-monorepo. Его ключевая фишка — affected graph: Nx понимает, какие пакеты изменились, и запускает только их тесты и билды. Не трогал компоненты в админке? Nx не будет их пересобирать. Магия, но не совсем — это граф зависимостей, построенный на основе импортов.
{
"tasksRunnerOptions": {
"default": {
"runner": "nx/tasks-runners/default",
"options": {
"cacheableOperations": ["build", "test", "lint"],
"parallel": 3
}
}
}
}Turborepo (Vercel) — более лёгкий, но не менее мощный. Основное отличие: Turborepo проще в настройке и лучше интегрируется с Vercel-экосистемой. Кэширование build-кэша между CI-запусками — одна из killer features.
nx-кэш: если вы запускаете билд, Nx кэширует результат. Запустили снова без изменений? Nx просто отдаёт результат из кэша. На CI это даёт 10-50x ускорение.
# Первый запуск — 5 минут
nx build web-app
# Второй запуск без изменений — 0.1 секунды (из кэша)
nx build web-app#Decision: что выбрать?
| Критерий | Polyrepo | Monorepo |
|---|---|---|
| Размер компании | 1-3 команды по разным продуктам | Единый продукт, несколько команд |
| Shared code | Минимум | Много общего |
| Team autonomy | 🟢 Полная изоляция | 🟡 Нужен coordination |
| Обновление зависимостей | 🔴 Pain (N репозиториев) | 🟢 Один PR |
| Build time | 🟢 Быстро (маленькие проекты) | 🟡 Может быть долго без кэша |
| CI complexity | 🟢 Простая | 🔴 Нужен affected graph |
| Git history | 🟢 Чистая | 🔴 Гигантский git |
| Cost | 🟢 Низкие (дешёвый CI) | 🟡 Средние (CI, Nx Cloud) |
Мой совет: Начинайте с monorepo, даже если вы стартап. Инструменты стали настолько хороши, что цена monorepo — минимальна. Переход с polyrepo на monorepo потом будет стоить в 10 раз дороже, чем начать с monorepo сразу.
#Micro-frontends: разделяй, но не умножай сущности
Micro-frontends — это логическое продолжение микросервисной архитектуры, но на клиентской стороне. Идея: разные команды владеют разными частями UI, разрабатывают их независимо и интегрируют в единое приложение.
Звучит классно. На практике — дьявольски сложно.
#Почему вдруг понадобились micro-frontends
Та же причина, что и для микросервисов: команды растут, а кодовая база превращается в монолит, который тормозит разработку.
Симптомы:
- У вас 6 команд, и каждая хочет менять свою часть страницы
- Релизный цикл — самый медленный в команде (все ждут всех)
- «Кто отвечает за хедер?» — никто не знает, но каждый его меняет
- Одна команда хочет перейти на Angular, другая — остаться на React
Micro-frontends декларируют: каждая команда делает свой кусок UI от и до — от API до пикселя. Независимый деплой, независимый выбор технологий, независимая ответственность.
#Как это выглядит на практике
Есть несколько способов склеить micro-frontends в одно приложение:
Webpack Module Federation — самый популярный подход (спасибо Webpack 5). Команда А публикует свой модуль как remote entry. Команда Б подключает его как remote:
// webpack.config.js — Host приложение
new ModuleFederationPlugin({
name: "shell",
remotes: {
"checkout": "checkout@https://cdn.checkout.doppler.com/remoteEntry.js",
"search": "search@https://cdn.search.doppler.com/remoteEntry.js",
},
shared: {
react: { singleton: true, requiredVersion: "^18.0.0" },
"react-dom": { singleton: true },
},
});Module Federation позволяет шарить зависимости (shared: react), чтобы не загружать по 5 копий React в браузере. Это критически важно для performance.
qiankun — микро-фронтенды для тех, кто не хочет думать. Фреймворк, который сам управляет жизненным циклом приложений, изоляцией стилей и sandboxing JavaScript. Популярен в Китае (Ant Financial, Alibaba), получает traction и на западе.
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: 'app-checkout',
entry: '//localhost:3002',
container: '#subapp-container',
activeRule: '/checkout',
},
]);
start({ sandbox: { experimentalStyleIsolation: true } });Single-SPA — мета-фреймворк, который объединяет разные frontend-приложения в одно. Работает с React, Vue, Angular, Svelte — всеми сразу. Проще, чем qiankun, но требует больше ручной настройки жизненного цикла.
Podium — от FINN (бывший Schibsted). Оригинальный подход: серверный композит, где каждый micro-frontend — это HTTP endpoint, который возвращает готовый HTML-фрагмент. Сервер склеивает их в одну страницу.
#Компромиссы, о которых молчат конференции
Micro-frontends решают организационные проблемы, но создают технические:
Производительность. Каждый micro-frontend загружает свои зависимости. Даже с Module Federation shared — shared only extends so far. Практика: если у вас 5 micro-frontends, каждому нужны свои CSS-инлайны, свои svg-иконки, свои утилиты. Размер бандла растёт нелинейно.
User Experience. Представьте: checkout написан на Angular, а страница товара — на React. Пользователь кликает по навбару (React) и ждёт 200ms, пока Angular-приложение инициализируется. Этот лаг — цена подхода.
Интеграционное тестирование. Как вы тестируете сценарий, который проходит через 3 micro-frontends? Если у вас нет E2E-тестов, покрывающих всю интеграцию, — у вас нет гарантий, что система работает.
Shared design system. Если у вас нет дизайн-системы, которую используют все micro-frontends, у вас будет визуальный хаос: кнопки разного цвета, шрифты разного размера, анимации разной скорости.
#Когда micro-frontends — правильное решение
Опыт DoorDash (о котором ниже) и других компаний показывает: micro-frontends работают, когда:
- Команда > 30 фронтенд-инженеров
- Есть зрелая дизайн-система (shared, versioned, tested)
- Есть выделенная платформенная команда, которая поддерживает shell и shared deps
- CI/CD настроена до автоматизма
- Performance-мониторинг на месте (без RUM вы слепы, см. ниже)
Когда micro-frontends — overkill:
- Команда < 10 человек
- Один продукт, один стек
- Нет инфраструктуры для независимого деплоя
- «Мы просто хотим попробовать модульную федерацию, звучит модно»
#Feature Flags: архитектура на 10 млн флагов
Feature flags (или feature toggles) — это механизм включения/выключения функциональности без деплоя. Звучит тривиально. На масштабе превращается в самостоятельный архитектурный слой.
#Почему feature flags — это архитектурное решение
Без feature flags каждый релиз — это событие. «В четверг в 18:00 деплоим checkout v3, всем быть на связи, в случае чего — откатываем». С feature flags релиз — это переключение тумблера: «Включили новую фичу на 5% пользователей, смотрим метрики, если всё хорошо — раскатываем до 100%».
Разница — в снижении когнитивной нагрузки и риска. Feature flags превращают релиз из операции с потенциальным даунтаймом в рутинное действие.
Но у масштаба есть цена.
#Проблемы на миллион флагов
Когда в системе 10+ миллионов активных флагов (да, LaunchDarkly обслуживает такие масштабы), начинаются интересные проблемы:
State explosion. Флаг «new-checkout» может быть включён для 50% пользователей, для iOS в Европе, НО выключен для пользователей старой версии приложения. Каждый флаг — многомерное пространство состояний. Комбинация флагов — экспонента.
Performance. Проверка флага — это сетевой запрос или чтение из кэша. Если на странице 200 компонентов и каждый проверяет 3 флага — это 600 проверок. На каждую проверку latency 5ms — уже 3 секунды только на флаги. Решение: пакетная проверка флагов (batch evaluation) на стороне клиента.
// Плохо: каждая проверка — отдельный запрос
const isNewCheckout = await ldClient.variation("new-checkout", false);
const isDarkMode = await ldClient.variation("dark-mode", false);
// Хорошо: один запрос на все флаги, кэширование на клиенте
const flags = await ldClient.allFlags();
const isNewCheckout = flags["new-checkout"];
const isDarkMode = flags["dark-mode"];Stale flags. Исследования показывают: ~30% флагов в системе мертвы (уже не используются, но код остался). Мёртвые флаги — это технический долг: код, который никогда не выполняется, но его нужно поддерживать, тестировать и не сломать при рефакторинге.
Testing matrix. Если фича зависит от состояния трёх флагов — у вас 2³ = 8 комбинаций для тестирования. А если 10 флагов? 1024 комбинации. Никто не тестирует все. Значит, какие-то комбинации никогда не проверяются — и ломаются при первом включении.
#Инструменты: LaunchDarkly — стандарт индустрии
LaunchDarkly — лидер рынка feature management. Они решают проблемы выше:
- Streaming updates: флаги доставляются через Server-Sent Events (SSE) — изменение флага на сервере отражается на клиенте за < 100ms. Без перезагрузки страницы.
- Targeting rules: включить для пользователей с email@company.com, для 10% случайных, для внутреннего staging-окружения.
- Prerequisite flags: флаг B не может быть включён, если выключен флаг A. Спасает от комбинаторного взрыва.
- Flag lifecycle management: напоминания «этот флаг не менялся 6 месяцев — может, пора удалить?»
// LaunchDarkly — пример использования
import { init } from "@launchdarkly/node-server-sdk";
const client = init(process.env.LAUNCHDARKLY_SDK_KEY);
app.get("/checkout", async (req, res) => {
const user = { key: req.userId, custom: { tier: req.userTier } };
const flags = await client.allFlagsState(user);
if (flags.getFlagValue("new-checkout-flow")) {
// new checkout
} else {
// old checkout
}
});#Unleash — open-source альтернатива
Unleash — если вы не хотите платить LaunchDarkly (и готовы администрировать ещё один сервис). Основные отличия:
- Open Source (Apache 2.0), можно хостить у себя
- Меньше функций, но базовый набор покрывает 80% потребностей
- Activate/deactivate стратегии через server-side SDK
- API-first design — всё, что делает Unleash, можно сделать через API
import { Unleash } from 'unleash-client-node';
const unleash = new Unleash({
url: 'https://your-unleash-instance.com/api',
appName: 'frontend-web',
environment: 'production',
});
unleash.on('ready', () => {
if (unleash.isEnabled('new-checkout-flow')) {
renderNewCheckout();
}
});#Feature flags hygiene
Правила, которые спасут вам жизнь:
- Удаляйте мёртвые флаги. Сделайте это частью Definition of Done: «фича выкачена на 100%, флаг удалён из кода». Лучший способ — автоматизировать: CI проверяет, что нет флагов старше N дней.
- Не используйте флаги для branching. Флаг на неделю жизни — ок. Флаг на год — архитектурная проблема.
- Измеряйте overhead. Каждый флаг добавляет complexity. Если у вас 500 активных флагов — у вас 500 точек, где код может сломаться.
- Стратегия default-значений. Что будет, если LaunchDarkly недоступен? Если default = false (фича выключена) — пользователь видит старый, рабочий код. Всегда так делайте.
#A/B Testing инфраструктура
Feature flags — это про включение/выключение. A/B testing — про «какой вариант лучше». Казалось бы, одно вытекает из другого. На практике A/B testing — это отдельный зверь.
#Проблема: статистика сложная
Большинство команд думают, что A/B-тест — это «разделить пользователей на две группы и сравнить конверсию». На деле:
- Multiple comparisons problem: вы сравниваете 10 метрик → вероятность ложного срабатывания ~40%, не 5%
- Novelty effect: пользователи кликают на новую кнопку, потому что она новая, а не потому что она лучше
- Network effects: тест влияет не только на user в тесте, но и на тех, кто с ним взаимодействует (social features)
#Байесовские методы vs Частотная статистика
Традиционный подход (частотная статистика, p-value) имеет проблему: он не говорит «какова вероятность, что вариант B лучше A». Он говорит «если бы разницы не было, какова вероятность увидеть такие данные?». Смысл разный, и это сбивает с толку.
Байесовский подход даёт то, что нужно бизнесу: «P(выигрыш | данные)» — вероятность, что B лучше A, с учётом того, что мы наблюдали.
# Упрощённый байесовский A/B тест (PyMC)
import pymc as pm
with pm.Model() as ab_model:
# Prior: мы не знаем, какая конверсия, но предполагаем ~5%
p_A = pm.Beta("p_A", alpha=1, beta=19) # prior: mean 5%
p_B = pm.Beta("p_B", alpha=1, beta=19)
# Likelihood: наблюдаем N конверсий из N попыток
obs_A = pm.Binomial("obs_A", n=n_A, p=p_A, observed=conversions_A)
obs_B = pm.Binomial("obs_B", n=n_B, p=p_B, observed=conversions_B)
# Inference
delta = pm.Deterministic("delta", p_B - p_A)
# Результат: P(delta > 0 | данные) — вероятность, что B лучшеPyMC позволяет делать это за 50 строк кода.
#Архитектура A/B платформы
В крупной компании инфраструктура A/B тестирования выглядит так:
- Assignment service — решает, в какой группе пользователь. Должен быть детерминированным (один user всегда в одной группе для одного эксперимента), быстрым (< 5ms), идемпотентным.
- Event pipeline — собирает данные о поведении пользователей (клики, просмотры, конверсии) в real-time.
- Statistical engine — считает результаты. Байесовский, с коррекцией на multiple comparisons.
- Dashboard — показывает: «группа B выигрывает с вероятностью 97%, но на маленькой выборке, подождите ещё».
#Практический паттерн: feature flag + A/B
В LaunchDarkly A/B-тесты работают поверх feature flags:
Feature flag: new-recommendations
└── Variation A (control): старые рекомендации (50%)
└── Variation B (test): ML-рекомендации (50%)Когда тест завершён и B выиграл — variation B становится вариацией по умолчанию, а feature flag фиксируется. Без нового деплоя.
#Performance at Scale: Core Web Vitals, INP, CLS
В маленьком проекте производительность — это приятная опция. В крупном — это SLA. Если ваш сайт — лицо компании (e-commerce, SaaS, медиа), каждая миллисекунда задержки стоит денег.
Цифры, которые нужно знать: Amazon подсчитал, что 100ms задержки стоят 1% выручки. Google — 0.5 секунды задержки снижают поисковый трафик на 20%. Для Walmart каждая 1 секунда улучшения LCP давала +2% конверсии.
#Core Web Vitals — язык, на котором с вами говорит Google
С 2021 года Core Web Vitals — это фактор ранжирования в Google. Но важнее не ранжирование, а то, что эти метрики действительно отражают пользовательский опыт.
Три метрики:
LCP (Largest Contentful Paint) — когда загрузился самый большой элемент на странице (герой-изображение, заголовок, видео). Цель: < 2.5 секунды.
Что реально влияет на LCP:
- Время до первого байта (TTFB) — если сервер отвечает 1 секунду, вы уже проиграли
- Resource loading — изображения должны быть оптимизированы, загружаться с CDN, иметь правильные размеры
- Render-blocking resources — CSS и JS, которые блокируют рендеринг
<!-- Хорошо: preload для LCP-изображения -->
<link rel="preload" href="hero.webp" as="image" fetchpriority="high">CLS (Cumulative Layout Shift) — насколько «дёргается» страница при загрузке. Если элементы прыгают (баннер загрузился → контент уехал вниз) — это CLS. Цель: < 0.1.
Что вызывает CLS:
- Изображения без
widthиheight - Динамическая реклама, которая вставляется после загрузки контента
- Web-шрифты с FOIT/FOUT (Flash of Invisible/Unstyled Text)
- Инжекты сторонних скриптов (виджеты, чаты)
/* Решение: резервируем место под изображение */
img {
aspect-ratio: 16 / 9;
width: 100%;
height: auto;
}
.hero-banner {
min-height: 400px; /* резервируем место под баннер */
}INP (Interaction to Next Paint) — метрика, заменяющая FID (First Input Delay) с марта 2024. Измеряет задержку между пользовательским взаимодействием (клик, тап, клавиатура) и следующим отрисованным кадром. Цель: < 200ms.
INP — самая сложная метрика по двум причинам:
- Она измеряет все взаимодействия на странице, не только первое
- Она включает не только главный поток (main thread), но и обработчики событий, рендеринг, layout
Как улучшить INP:
- Избегайте долгих задач (> 50ms на главном потоке)
- Выносите тяжёлые вычисления в Web Workers или былайзеры (isInputPending)
- Используйте
requestAnimationFrameиscheduler.yield()для разбивки работы
// scheduler.yield() — даёт браузеру шанс отреагировать на ввод
async function handleExpensiveTask() {
for (const item of largeArray) {
processItem(item);
// После каждого элемента — даём браузеру передохнуть
if (shouldYield()) {
await scheduler.yield();
}
}
}#Performance Budgets: держим архитектуру в узде
Самый эффективный способ не потерять производительность — поставить бюджет и умирать на CI, если он превышен.
// webpack.config.js — performance budget
module.exports = {
performance: {
maxAssetSize: 244000, // 244KB per asset
maxEntrypointSize: 500000, // 500KB per entrypoint
hints: "error", // CI упадёт при превышении
},
};Для крупных проектов этого недостаточно. Нужно:
- Budget per route — страница логина не должна весить столько же, сколько дашборд
- Budget per component — если компонент тяжелее X, он lazy-loaded
- Budget per third-party — каждый сторонний скрипт — это N ms к LCP. Разрешайте их через process
#Практика: Bundle Analysis
# Анализ бандла
npx esbuild-visualizer --metafile meta.json
npx source-map-explorer dist/*.jsНа регулярной основе:
# .github/workflows/bundle-analysis.yml
- name: Analyze Bundle
run: npx bundlesize
env:
BUNDLESIZE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}#RUM: Real User Monitoring
Lighthouse даёт лабораторные метрики (lab data) — как быстро сайт загружается у вас в кабинете на MacBook Pro. Пользователям плевать на ваши лабораторные данные. Их волнует реальный опыт — RUM (Real User Monitoring).
#Почему lab data не работают
У вас отличный Lighthouse score — 95+. Но пользователи жалуются, что сайт тормозит. Классическая ситуация.
Разрыв между лабораторными данными и реальностью:
- Пользователи на дешёвых Android-смартфонах с 3G — ваш Lighthouse на MBP 3x быстрее
- Пользователи с 200 вкладок в Chrome — браузер экономит ресурсы
- Расширения, блокировщики рекламы, корпоративные VPN — все влияют на скорость
- Время суток, география, загрузка сервера — в лаборатории этого нет
RUM нужен именно для этого: измерять то, что происходит в реальном мире.
#Архитектура RUM-системы
flowchart LR
Browser -->|Performance API| Collector
Collector -->|Stream processing| Aggregator
Aggregator -->|Metrics| Dashboard
Aggregator -->|Alerts| AlertingКоллектор — принимает данные с браузеров. Миллионы запросов в минуту. Должен быть:
- Лёгким (1x1 pixel GIF или Beacon API — не влияет на производительность)
- Быстрым (ждём < 100ms, пишем асинхронно в очередь)
- Надёжным (не теряем данные, но и не замедляем сайт)
// Сбор RUM-данных на клиенте
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const data = entries.map(e => ({
name: e.name,
type: e.entryType,
duration: e.duration,
startTime: e.startTime,
url: window.location.pathname,
connection: navigator.connection?.effectiveType,
deviceMemory: navigator.deviceMemory,
}));
// Beacon API — отправляем, когда браузеру удобно, без блокировки
navigator.sendBeacon('/rum', JSON.stringify(data));
});
observer.observe({ type: 'largest-contentful-paint', buffered: true });
observer.observe({ type: 'layout-shift', buffered: true });
observer.observe({ type: 'first-input', buffered: true });
observer.observe({ type: 'interaction', buffered: true }); // INPАгрегатор — собирает данные и считает перцентили. Ключевое: смотреть не среднее (mean), а P75, P90, P95.
P50 LCP: 1.8s ← «у половины пользователей хорошо»
P75 LCP: 2.9s ← «у 25% пользователей плохо»
P95 LCP: 8.2s ← «у 5% пользователей катастрофа»Среднее LCP = 2.3s — звучит неплохо. P95 = 8.2s — это команда «лечить срочно».
#RUM-провайдеры
- Google Analytics 4 — базовый, бесплатный, но сэмплинг данных страдает
- Datadog RUM — мощный, с distributed tracing (какой запрос в бэкенд вызвал медленный LCP?)
- New Relic Browser — full-stack observability, от клика до database query
- SpeedCurve — заточен под Core Web Vitals и performance budgets
- Vercel Analytics — просто, если вы на Vercel, бесплатно для старта
- Plausible — privacy-first альтернатива, лёгкая, но меньше RUM-возможностей
#Alerting на RUM
Правило: RUM-алерты должны бить, когда реальным пользователям плохо, а не когда P50 упал на 50ms.
# Пример алерта: P75 LCP превысил 3s для аудитории на 4G
alert:
metric: rum.lcp.p75
threshold: 3000
filter: effective_connection_type = "4g"
condition: ">"
duration: 5m
on: 10% traffic increase#CI/CD для фронтенда: Preview Deployments, Percy, Chromatic
CI/CD для фронтенда отличается от бэкендного. У бэкенда ключевой вопрос: «Работает ли API?». У фронтенда: «Как это выглядит? Не сломалось ли что-то?».
#Preview Deployments — смотри, не трогай руками
Preview deployment — это временная среда для каждого PR или ветки. Вы открываете PR — CI разворачивает версию вашего фронтенда на уникальный URL. Вы можете показать её ревьюверу, дизайнеру, продакт-менеджеру — без деплоя на продакшн.
Vercel Preview Deployments — «золотой стандарт». Каждый PR → уникальный URL. Интеграция с GitHub. Автоматические комментарии к PR с ссылкой на preview.
Netlify Deploy Previews — то же самое. Хорошо работает с Gatsby, Next.js, Astro.
Cloudflare Pages Previews — альтернатива для тех, кто уже на Cloudflare.
# GitHub Actions + Vercel Preview
name: Preview
on: pull_request
jobs:
preview:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }}Почему это важно для архитектуры: preview deployments убирают единственный bottleneck в code review. Ревьювер не гадает, как выглядит фича — он видит её. И находим проблемы на этапе PR, а не через неделю после деплоя.
#Visual Regression Testing — Percy и Chromatic
У вас есть тесты. Unit-тесты, integration-тесты, E2E-тесты. Все зелёные. Вы деплоите — и оказывается, что какой-то CSS-класс поменял отступы во всём UI. Потому что кто-то обновил дизайн-систему, а border-radius изменился, и теперь все карточки выглядят странно.
Visual regression testing — это ответ. Идея: берётся screenshot до изменений (baseline), screenshot после, и сравнивается. Pixel-perfect diff.
Percy (BrowserStack) — лидер на рынке. Интеграция с CI: на каждый PR Percy делает скриншоты компонентов и страниц, показывает diff.
// Percy + Cypress
import percySnapshot from '@percy/cypress';
describe('Checkout Page', () => {
it('should match visual snapshot', () => {
cy.visit('/checkout');
percySnapshot('Checkout Page — default state');
cy.get('[data-test="add-to-cart"]').click();
percySnapshot('Checkout Page — item added');
});
});Chromatic (Chromatic.com) — визуальное тестирование для Storybook. Публикуете Storybook, Chromatic делает скриншоты историй, находит изменения, показывает diff в UI.
# .github/workflows/chromatic.yml
- name: Publish to Chromatic
uses: chromaui/action@latest
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
autoAcceptChanges: true # автоматически принимать одинаковые скриншоты#Фронтенд-пайплайн: что должно быть в CI
Минимальный фронтенд-пайплайн:
jobs:
quality:
steps:
- lint: "eslint, prettier, tsc — проверка типов"
- test: "vitest, jest — unit + integration"
- build: "проверка, что проект собирается"
- bundle-analysis: "проверка performance budget"
visual-regression:
needs: quality
steps:
- percy: "сравнение скриншотов страниц"
- chromatic: "сравнение скриншотов Storybook компонентов"
e2e:
needs: quality
steps:
- playwright/cypress: "критические user flows"
preview-deploy:
needs: quality
steps:
- vercel/netlify: "временный URL для PR"Каждый этап — быстрый фидбек. Lint упал за 30 секунд — вы знаете проблему до того, как запустился build на 5 минут.
#Кейс: DoorDash — эволюция фронтенда
DoorDash — один из лучших примеров того, как фронтенд-архитектура эволюционирует вместе с компанией. От монолита, через микро-фронтенды к платформенному подходу.
#2014-2016: jQuery + Rails — «лишь бы работало»
В начале DoorDash был классическим стартапом: монолит на Ruby on Rails, jQuery на фронтенде. Быстро, дёшево, работает.
Фронтенд генерировался на сервере: Rails partials + jQuery для интерактивности. Страница загружалась за 200-300ms. Разработчиков было 5. Проблем с архитектурой не было — была одержимость скоростью доставки.
#2016-2018: React SPA — «настоящий фронтенд»
Команда выросла до 20 разработчиков. Функциональность умножилась: дашборды для ресторанов, трекинг доставки в реальном времени, динамическое ценообразование.
Rails-монолит начал тормозить: каждая новая страница требовала изменений в Ruby, деплой занимал всё больше времени, фронтенд-код был размазан по ERB-шаблонам.
Решение: перейти на React SPA. Backend API — новый сервис на Python + uWSGI. Frontend — Create React App + Redux.
Результат: скорость разработки выросла, UX улучшился, но появились новые проблемы:
- Bundle size: 2MB+ на загрузку (привет пользователям с 3G)
- «API-несоответствие»: каждый экран требовал своих данных, приходилось делать 5-7 запросов на страницу
- Разрастание Redux store: всё, что было на странице, попадало в Redux, store весил мегабайты
#2018-2020: Micro-frontends — масштабирование команд
К 2018 году у DoorDash было 60+ фронтенд-инженеров и несколько продуктовых линий: Consumer (дашборд клиента), Dasher (приложение курьера), Merchant (панель ресторана) и Admin (внутренняя админка). Каждая — со своим темпом релизов, стеком и требованиями.
Решение: micro-frontends на Webpack Module Federation. Единый shell, который загружает микросервисы по мере необходимости.
Архитектура:
Shell (shared: react, design system, routing)
├── Consumer App (React)
├── Merchant App (React)
├── Dasher App (Preact — лёгкий клиент для курьеров)
└── Admin App (React + Table-ориентированные компоненты)Module Federation позволил:
- Деплоить каждое приложение независимо
- Разным командам использовать разные версии React (shell shared только часть)
- Дизайн-систему — как shared module, одна версия для всех
- Постепенно переходить на новые технологии без переписывания всё сразу
Проблемы, с которыми столкнулись:
- Performance: каждый micro-frontend — это отдельный JS-бандл. На старте — 6 отдельных бандлов → LCP выросло с 1.5s до 2.8s. Пришлось вводить code splitting на уровне shell.
- Testing: E2E-тесты, проходящие через несколько micro-frontends, были медленными (30+ минут). Перешли на contract testing.
- Shared state: shell не должен знать о состоянии дочерних приложений. Решили через события (Custom Events + postMessage), а не shared store.
- Dev experience: локальная разработка с 5 micro-frontends — это Docker Compose, hot reload не работал. Сделали tooling: монорепозиторий с Turborepo.
#2020-2022: Платформенный подход — зрелость
К 2020 году DoorDash осознал: micro-frontends решили организационные проблемы, но создали технические. Пришла платформенная команда.
Что они сделали:
- Feature flags platform: собственная система на основе LaunchDarkly. Позволила A/B-тестировать изменения на уровне фич, а не целых приложений.
- Design system: Platform UI — библиотека компонентов, общая для всех micro-frontends. Строгое версионирование, semver, CI/CD с Percy.
- Performance monitoring: RUM на Datadog + Web Vitals в реальном времени. Каждый micro-frontend имеет свой performance dashboard.
- Developer portal: Backstage (Spotify) для каталога всех сервисов и документации.
Цифры (2023):
- 300+ фронтенд-разработчиков
- 15+ micro-frontends в production
- 5000+ активных feature flags
- 200+ A/B экспериментов одномоментно
- P95 LCP: 3.2s (было 8s в начале micro-frontends)
- CI пайплайн: 15 минут (было 45+)
#Уроки DoorDash
- Не начинайте с micro-frontends. Начните с хорошего монолита. Micro-frontends — это решение для масштаба, а не для моды.
- Платформа должна предшествовать micro-frontends. Если у вас нет дизайн-системы, CI/CD и feature flags — micro-frontends не помогут.
- Инвестируйте в performance monitoring. Без RUM вы не знаете, насколько ваши архитектурные решения ухудшили опыт пользователей.
- Feature flags — это необходимый эволюционный шаг. DoorDash не мог бы делать 200+ экспериментов без LaunchDarkly-подобной системы.
- Религиозных войн — не надо. Команда DoorDash прагматична: React для сложных UI, Preact для мобильных курьеров, shell на vanilla JS для минимального размера. Инструмент под задачу, а не наоборот.
Cross-reference: Подход DoorDash к feature flags и платформенному мышлению перекликается с тем, что мы обсуждали в Главе 8, секция «Design Systems» — когда дизайн-система становится платформой, а не просто библиотекой компонентов.
#Когда это применимо
#Стартап (1-10 разработчиков)
- Monorepo: да, начните с него (Turborepo, Nx)
- Micro-frontends: нет, категорически
- Feature flags: можно LaunchDarkly или Unleash, но пока базово
- Performance monitoring: Lighthouse CI + Web Vitals — бесплатного хватит
- Preview deployments: Vercel или Netlify — встроено, настраивается за 10 минут
#Средняя компания (10-50 разработчиков)
- Monorepo: да, Nx обязателен (без графа зависимостей — замучаетесь ждать билды)
- Micro-frontends: только если команды действительно независимы. Иначе — модульный фронтенд, но в одном репозитории
- Feature flags: LaunchDarkly — must have. Не пытайтесь написать свою систему флагов (автор знает, о чём говорит)
- Performance budgeting: обязателен на CI
- RUM: SpeedCurve или Datadog RUM — не экономьте на этом
- Visual regression: Percy или Chromatic — сэкономит вам недели отладки CSS
#Enterprise (50+ разработчиков)
- Monorepo: да, с Nx Cloud или собственным build farm
- Micro-frontends: только если есть платформенная команда. Иначе — вместо микро-фронтендов получите распределённый монолит
- Feature flags: LaunchDarkly Enterprise — с аудитом, compliance, SSO
- Performance: full RUM + lab data + SLA с бизнесом (LCP < 2.5s для 90% пользователей)
- Внутренняя A/B платформа: скорее всего, своя надстройка над LaunchDarkly/Statsig
#Чек-лист для самопроверки
- Monorepo или polyrepo — выбор осознанный, а не «так исторически сложилось»
- Если micro-frontends — есть платформенная команда, дизайн-система и CI/CD
- Feature flags — не только tool, но и process (удаление мёртвых флагов — часть Definition of Done)
- A/B тестирование — с коррекцией на multiple comparisons, а не «просто split по user_id»
- Performance budget — подключён на CI и проваливает сборку при превышении
- RUM — данные собираются с реальных пользователей, P75/P90/P95 считается, алерты настроены
- LCP < 2.5s, CLS < 0.1, INP < 200ms для P75 вашей аудитории
- Preview deployments — каждый PR получает URL, на котором можно посмотреть изменения
- Visual regression — Percy/Chromatic на каждый PR
- CI пайплайн < 15 минут (если дольше — вы теряете разработчиков в переключении контекста)
#«Не делай так»
#Анти-паттерн: Monorepo без инструментов
Взяли большой проект, сложили всё в один репозиторий. Без Nx, без Turborepo, без кэширования. Просто npm workspaces.
Результат:
npm install— 3 минуты (потому что 200 пакетов)npm test— 45 минут (все тесты всех пакетов)- CI — 60+ минут, разработчики успевают сходить за кофе и обратно
Лечение: Nx с графом зависимостей и кэшированием. Никакого monorepo без affected graph.
#Анти-паттерн: Micro-frontends ради micro-frontends
«У нас 3 разработчика и одна кодовая база на 50K строк. Давайте сделаем micro-frontends!»
Зачем? Потому что «так делает Spotify» (спойлер: Spotify делает Backstage и модульный монолит, а не микро-фронтенды).
Результат:
- Настройка Module Federation заняла 2 недели
- Бандл вырос на 40% (shared deps не идеальны)
- State management стал адом
- Разработка замедлилась в 2 раза
- Через 6 месяцев откатились обратно
Лечение: Не делайте micro-frontends, пока монолит не стал тормозить разработку. А когда стал — начните с модульного монолита внутри одного репозитория. Micro-frontends — это последнее средство, не первое.
#Анти-паттерн: Тысяча флагов без системы
Завели feature flags прямо в коде: if (localStorage.getItem('new-ui') === 'true'). Никакой LaunchDarkly, никакого Unleash.
Через год:
- 500+ флагов, никто не знает, какие работают
- Код, обёрнутый в флаги, никто не рефакторит
- Тестировать невозможно — «включи эти 3 флага, и будет то, что надо»
- Пришлось нанимать отдельную команду, чтобы чистить legacy flags
Лечение: Feature flags — это система, а не паттерн. Если вы не готовы администрировать LaunchDarkly или Unleash — не используйте флаги вообще. Используйте ветки и feature branches.
#Анти-паттерн: RUM без действий
Запустили RUM. Datadog показывает всё: LCP, CLS, INP, custom metrics. Красивые дашборды. Но никто не смотрит на них. Когда LCP вырос с 2.5s до 4s — узнали через месяц, когда пользователи начали жаловаться в поддержку.
Лечение: RUM без алертов и runbook — это дорогой дашборд. Настройте: «Если P75 LCP > 3s для 10% трафика — отправляем в Slack команды и заводим тикет».
#Анти-паттерн: Preview deployments без автоматического тестирования
Сделали preview deployments. Каждый PR — свой URL. Классно. Но кто проверяет этот URL? Тот же разработчик, который написал код. Он нажимает «смотрится», «работает» — и мерджит. Через неделю баг, который был виден на preview, — в продакшене.
Лечение: Preview deployment — это не замена тестированию. Это платформа для тестирования. Percy/Chromatic + Playwright/Cypress должны бежать на каждый preview. И только после зелёных тестов — код-ревью человеком.
#Ключевые выводы
-
Monorepo — стандарт для крупных фронтендов. Начните с Turborepo или Nx. Переход с polyrepo на monorepo стоит дорого — сделайте правильный выбор в начале.
-
Micro-frontends — для организационных проблем, а не технических. Если ваша проблема — 10 команд на одном фронтенде, micro-frontends помогут. Если ваша проблема — 3 команды и медленный билд — начните с модульного монолита и хорошего tooling.
-
Feature flags — эволюционный шаг, который меняет релизный процесс. Без них вы не сможете делать A/B-тесты, канареечные деплои и постепенные раскатки. Но флаги — это ответственность: мёртвые флаги надо чистить, иначе они становятся техническим долгом.
-
Performance — это SLA, а не опция. Core Web Vitals — это минимум. RUM — это то, как вы узнаете, что ваш минимум недостижим для 25% пользователей. Инвестируйте в мониторинг до того, как пользователи начнут жаловаться.
-
CI/CD для фронтенда включает три уровня: функциональное тестирование (unit → integration → E2E), визуальное тестирование (Percy/Chromatic) и preview deployments. Без визуального тестирования вы не заметите, что сломали UI, пока это не увидят пользователи.