Часть VI: Крупные проекты — архитектура на уровне гигантов
Глава 26

Архитектура крупных фронтендов — когда фронтенд — это не «просто скрипты»

Проблема

Проблема: Фронтенд перестал быть прослойкой между бэкендом и браузером. Когда кодовая база переваливает за миллион строк, команда — за 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

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

Правила, которые спасут вам жизнь:

  1. Удаляйте мёртвые флаги. Сделайте это частью Definition of Done: «фича выкачена на 100%, флаг удалён из кода». Лучший способ — автоматизировать: CI проверяет, что нет флагов старше N дней.
  2. Не используйте флаги для branching. Флаг на неделю жизни — ок. Флаг на год — архитектурная проблема.
  3. Измеряйте overhead. Каждый флаг добавляет complexity. Если у вас 500 активных флагов — у вас 500 точек, где код может сломаться.
  4. Стратегия 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 тестирования выглядит так:

  1. Assignment service — решает, в какой группе пользователь. Должен быть детерминированным (один user всегда в одной группе для одного эксперимента), быстрым (< 5ms), идемпотентным.
  2. Event pipeline — собирает данные о поведении пользователей (клики, просмотры, конверсии) в real-time.
  3. Statistical engine — считает результаты. Байесовский, с коррекцией на multiple comparisons.
  4. 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 — самая сложная метрика по двум причинам:

  1. Она измеряет все взаимодействия на странице, не только первое
  2. Она включает не только главный поток (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 упадёт при превышении
    },
};

Для крупных проектов этого недостаточно. Нужно:

  1. Budget per route — страница логина не должна весить столько же, сколько дашборд
  2. Budget per component — если компонент тяжелее X, он lazy-loaded
  3. 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

  1. Не начинайте с micro-frontends. Начните с хорошего монолита. Micro-frontends — это решение для масштаба, а не для моды.
  2. Платформа должна предшествовать micro-frontends. Если у вас нет дизайн-системы, CI/CD и feature flags — micro-frontends не помогут.
  3. Инвестируйте в performance monitoring. Без RUM вы не знаете, насколько ваши архитектурные решения ухудшили опыт пользователей.
  4. Feature flags — это необходимый эволюционный шаг. DoorDash не мог бы делать 200+ экспериментов без LaunchDarkly-подобной системы.
  5. Религиозных войн — не надо. Команда DoorDash прагматична: React для сложных UI, Preact для мобильных курьеров, shell на vanilla JS для минимального размера. Инструмент под задачу, а не наоборот.
Cross-reference

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. И только после зелёных тестов — код-ревью человеком.


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

  1. Monorepo — стандарт для крупных фронтендов. Начните с Turborepo или Nx. Переход с polyrepo на monorepo стоит дорого — сделайте правильный выбор в начале.

  2. Micro-frontends — для организационных проблем, а не технических. Если ваша проблема — 10 команд на одном фронтенде, micro-frontends помогут. Если ваша проблема — 3 команды и медленный билд — начните с модульного монолита и хорошего tooling.

  3. Feature flags — эволюционный шаг, который меняет релизный процесс. Без них вы не сможете делать A/B-тесты, канареечные деплои и постепенные раскатки. Но флаги — это ответственность: мёртвые флаги надо чистить, иначе они становятся техническим долгом.

  4. Performance — это SLA, а не опция. Core Web Vitals — это минимум. RUM — это то, как вы узнаете, что ваш минимум недостижим для 25% пользователей. Инвестируйте в мониторинг до того, как пользователи начнут жаловаться.

  5. CI/CD для фронтенда включает три уровня: функциональное тестирование (unit → integration → E2E), визуальное тестирование (Percy/Chromatic) и preview deployments. Без визуального тестирования вы не заметите, что сломали UI, пока это не увидят пользователи.