Архитектура фронтенда
Ревью архитектуры фронтенда: порядок и каскадные сбои
Команда эскалирует: дашборд тормозит на каждом нажатии клавиши, и они заводят тикет «добавить разбивку кода и лениво грузить графики». Ты профилируешь. Дело не в графиках. Нажатия в форме пишутся в глобальный стор, каждый подписчик перерендеривается на каждый символ, и поддерево графика — один из них. Разбивка бандла графика ничего не меняет — перерендер срабатывает уже после загрузки чанка. Фикс был тремя юнитами раньше: нажатия — это локальное UI-состояние, им не место в глобальном сторе. Команда оптимизировала последний слой, чтобы замазать баг первого.
Весь трек — это одно дерево решений
Каждый юнит до этого учил решению по отдельности: форма состояния, загрузка данных, доступные формы, дизайн-токены, границы монорепо, разбивка кода, build-пайплайны. В реальном приложении это не семь отдельных выборов — это один стек, и у стека есть направление. Нижние слои ограничивают верхние. Форма состояния решает радиус перерендеров; токены решают, будет ли ребрендинг сменой конфига или миграцией; границы монорепо решают время CI; разбивка кода и build-пайплайн решают, что реально уезжает в браузер.
Ход сеньора при ревью нового фронтенда — читать не сверху вниз (структура файлов, потом компоненты, потом состояние), а снизу вверх по цене сбоя. Самые дешёвые в починке баги — в пайплайне; самые дорогие — в форме состояния, потому что всё ниже по течению их наследует. Поэтому ты оцениваешь слои в том порядке, где ошибка наносит больше всего урона, и перестаёшь оптимизировать слой в тот момент, когда понимаешь, что настоящий баг живёт ниже.
Каскад: где всплывает сбой каждого слоя
Ловушка из Hook — определяющий паттерн: симптом появляется в одном слое, а причина — в нижнем. Перерендер-джанк выглядит как проблема рендера или бандла, поэтому команды тянутся к мемоизации и разбивке кода — верхним слоям — и сжигают недели. Причина была решением о форме состояния: высокочастотное состояние (нажатия) поднято в глобальный стор, поэтому каждый подписчик перерендеривается на каждое изменение. Никакой React.lazy это не чинит, потому что лишний рендер происходит уже после прихода чанка.
Прочитай каскад и в другую сторону. Отсутствие системы токенов всплывает как боль ребрендинга: маркетинговый ребрендинг, который должен быть однострочной сменой примитивных значений, превращается в hunt-and-replace по сотням файлов. Atlassian и любая зрелая дизайн-система структурируют токены в три слоя — примитивы (сырая палитра), семантика (что цвет значит: text-danger, surface-raised) и компонентные значения. Ребрендинг = сменить примитивы. Тёмная тема = сменить семантику, компоненты не трогаются. Команды, захардкодившие #1a73e8 повсюду, вместо этого платят неделями; одна задокументированная миграция 200 компонентов на тёмную тему заняла два дня с семантическим слоем токенов и заняла бы недели find-and-replace без него.
| Слой (порядок ревью) | Симптом при поломке | Почему верхний слой не починит |
|---|---|---|
| 1. Форма состояния | Перерендер-штормы, расходящиеся производные значения, устаревшие после мутации view | Разбивка/мемо работают после лишнего рендера; радиус взрыва задаёт то, где живёт состояние |
| 2. Загрузка данных | Водопады запросов, медленный LCP, двойные фетчи | Быстрый бандл всё равно сериализует N round-trip’ов, если граф фетчей последовательный |
| 3. Токены | Боль ребрендинга, несогласованный UI, переписывание тёмной темы | Захардкоженные значения — не проблема сборки; отсутствует источник истины |
| 4. Границы монорепо | Взрыв времени сборки, связанность пакетов, медленный CI | Бандлеры подчиняются твоему графу зависимостей; плохие границы пересобирают всё на любое изменение |
| 5. Разбивка кода | Огромный начальный бандл, медленный Time-to-Interactive | Чинится только здесь — но только если слои 1–4 в порядке |
| 6. Build-пайплайн | Уезжает мёртвый код, нет tree-shaking, медленный фидбек CI | Дешевле всего чинится; смена конфига, а не архитектуры |
Границы монорепо — множитель времени сборки
Слой, который чаще всего винят в медленном CI — «сборка медленная, купим раннеры побыстрее» — обычно это проблема границ, а не железа. Если у пакетов нет настоящих границ (всё импортит всё, один гигантский пакет shared, от которого зависит каждое приложение), то любое изменение инвалидирует кэш всего графа, и CI пересобирает всё. Фикс структурный: чистые границы пакетов плюс выполнение только затронутого (affected-only), чтобы изменение одного пакета собирало один пакет.
Цифры жёсткие. Mercari сообщил о сокращении длительности задач Turborepo примерно на 50% и общего времени CI-джоба на ~30% после добавления удалённого кэша и тюнинга воркфлоу. Задокументированные сетапы монорепо на GitHub Actions достигают 12-кратного сокращения CI, комбинируя affected-only-выполнение, удалённый кэш и динамическую матрицу; 15-минутная сборка падает до 2–3 минут, когда изменился только один пакет, а остальное — попадания в кэш, и удалённый кэш может убрать 80–90% общего времени сборки для большой команды, потому что каждый CI-ран делится работой вместо пересчёта. Но самый большой рычаг — выше любого кэша: выполнение только затронутого. Запустить 4 пакета вместо 45 бьёт любую оптимизацию кэширования, и до этого ты доходишь, только если границы настоящие. Быстрый бандлер не спасёт граф зависимостей, где каждый лист зависит от ствола.
Почему это работает
Почему ревьюить снизу вверх, а не по структуре файлов? Потому что слои образуют цепочку зависимостей, и фикс, применённый выше настоящей причины, — это потраченный впустую труд, который ещё и прячет баг. Тикет команды из Hook «добавить разбивку кода» уехал бы, чанк графика лениво загрузился бы, джанк остался бы, а постмортем обвинил бы не тот слой. Ревью в порядке цены сбоя означает, что ты сначала находишь самый нижний сломанный слой и чинишь там — и тогда симптом и все остальные симптомы, которые он породил, исчезают разом.
Build-пайплайн — самый дешёвый слой; чини его последним, а не первым
Пайплайн (конфиг бандлера, tree-shaking, минификация, интеграция с CI) — это место, с которого команды обожают начинать, потому что оно ощущается измеримым: включил флаг, смотришь, как падает цифра. Панель Coverage в Chrome DevTools регулярно показывает, что 20–30% уехавшего JavaScript никогда не выполняется на данной странице — реальные, возвращаемые трафик и время парсинга. Это стоит делать. Но это последнее, что ревьюит сеньор, потому что это смена конфига, а не архитектуры, а конфиг дёшево чинить потом. Оптимизировать пайплайн, пока сломана форма состояния, — инженерный эквивалент полировки машины с заклинившим двигателем: поверхность выглядит быстрее, машина — нет.
Дисциплина в том, чтобы порядок цены сбоя вёл ревью: подтверди форму состояния и граф фетчей (вместе они владеют интерактивностью и LCP — целься в LCP меньше 2.5 с), затем токены (цена ребрендинга/темизации), затем границы (время CI), и только потом разбивай бандлы и тюнь пайплайн. Каждый верхний слой предполагает, что нижние в порядке; переверни порядок — и будешь оптимизировать симптомы.
Новый внутренний инструмент: одно приложение, небольшая дизайн-команда с ребрендингом примерно раз в год, команда из 5 инженеров, ~12 запланированных пакетов, графики тяжёлые, но открываются редко. Выбери подходящую архитектуру.
Дашборд перерендеривается на каждое нажатие в поле поиска. Коллега предлагает разбить графики на чанки, чтобы починить. Как читает сеньор?
CI занимает 15 минут на однострочное изменение в одном из 12 пакетов. Что бьёт по корневой причине?
Расставь слои, которые сеньор оценивает при ревью нового фронтенда (сначала самое дорогое в починке):
- 1 Форма состояния — выводимое vs хранимое, серверный кэш vs клиентское состояние, колокация (задаёт радиус перерендеров)
- 2 Загрузка данных — водопады vs параллелизм, что владеет LCP
- 3 Дизайн-токены — слои примитивы→семантика, чтобы ребрендинг/тёмная тема были сменой, а не миграцией
- 4 Границы монорепо — чистые пакеты + affected-only CI, множитель времени сборки
- 5 Разбивка кода — route/компонентные чанки, lazy-load тяжёлого и редкого
- 6 Build-пайплайн — tree-shaking, удаление мёртвого кода, интеграция с CI (дешевле всего, чини последним)
- 01Команда винит тормозящий дашборд в отсутствии разбивки кода. Пройди, как ты нашёл бы настоящий слой и почему их фикс не сработает.
- 02Почему сеньор ревьюит архитектуру снизу вверх по цене сбоя, а не сверху вниз по структуре файлов, и какие слои самые дорогие, а какие — дешёвые?
Весь трек схлопывается в одно правило: слои фронтенда каскадируют, поэтому сеньор ревьюит их снизу вверх по цене сбоя, а не сверху вниз по структуре файлов. Форма состояния — самая дорогая, потому что задаёт радиус перерендеров — высокочастотное состояние в глобальном сторе даёт перерендер-штормы, которые не чинит ни разбивка кода, ни мемоизация, потому что они работают после лишнего рендера. Над ней — граф фетчей (водопады владеют LCP), затем токены (слой примитивы→семантика делает ребрендинг сменой значений, а не многонедельной охотой по файлам — два дня против недель для миграции тёмной темы), затем границы монорепо (множитель времени сборки; чистые границы плюс affected-only-выполнение и удалённый кэш превратили 15-минутную сборку в 2–3 минуты и сняли ~50% времени задач Mercari). Разбивка кода и build-пайплайн идут последними, потому что это самые дешёвые слои — конфиг, а не архитектура. Повторяющаяся ловушка — принимать симптом в верхнем слое за баг этого слоя; долговечный фикс всегда у самого нижнего сломанного слоя, и ревью в порядке цены сбоя — это как ты его находишь.