awesome-everything EN
↑ Обратно к восхождению

Архитектура фронтенда

Границы монорепозитория: граф зависимостей решает твой счёт за CI

Суть Монорепозиторий быстр, только если инструмент знает граф зависимостей и собирает лишь изменённое. Ошибись с границами — один пакет «shared utils», который импортируют все, — и каждый PR пересобирает весь мир.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на junior-высоте — поверхность
◷ 16 min

Правка двух строк текста на маркетинговой странице висит в CI 41 минуту. Изменение задело один компонент, но пайплайн пересобрал и протестировал все 38 проектов, потому что импорт резолвился через @acme/utils — пакет «shared utils», от которого зависит каждое приложение и каждая библиотека в репозитории. Тронешь что-то, что трогает utils, — и affected-граф становится всем репозиторием. Вердикт команды на ретро: «монорепозиторий тормозит». Тормозил не монорепозиторий. Неправильной была граница.

Граф зависимостей — это и есть продукт

Монорепозиторий — это не «весь код в одной папке». Это просто папка. Быстрым или медленным монорепозиторий делает граф зависимостей — направленный граф того, какой пакет какой импортирует. Turborepo и Nx строят этот граф, разбирая твои импорты и workspaces в package.json, и используют его для двух задач: упорядочивание (собрать библиотеку до приложения, которое её потребляет) и определение масштаба (вычислить радиус взрыва изменения).

Каждый узел — пакет или проект; каждое ребро — зависимость. Здоровый граф широкий и неглубокий: много листовых библиотек, мало кто зависит от любой одной из них. Больной граф имеет хаб — один узел с огромным fan-in, который лежит на пути между всем. Этот хаб обычно зовётся utils, common или shared, и это самая дорогая ошибка в монорепозитории, потому что алгоритм affected-графа хорош ровно настолько, насколько хорош граф, который ты ему дал.

«Affected» против собрать-всё

Наивный CI-пайплайн запускает build && test по каждому пакету в каждом PR. На 5 пакетах никто не замечает. На 40 полный прогон — это 30–45 минут, и очередь на мердж встаёт. Фикс — affected (Nx) / filtered (Turborepo): вычислить diff с базовой веткой, найти изменённые проекты, пройтись по графу и добавить каждый проект, который зависит от изменённого, и запустить задачи только для этого набора.

Цифры драматичны, когда граф чистый. Workspace на 20 пакетов обычно падает с ~14 мин (собрать всё) до 3–5 мин (только affected). Когда изменение локально для листа, affected-набор может быть одним проектом, и CI завершается меньше чем за минуту. На этом стоит вся ставка монорепозитория: большинство PR трогают маленький срез, поэтому большинство PR должны платить за маленький срез.

СтратегияЧто бежит на PR~CI на 40 пакетахРежим отказа
Собрать всёВсе проекты, всегда30–45 минРастёт с размером репо, не изменения
Только affectedИзменённые проекты + их зависимые1–5 мин (типичный PR)Хаб-пакет делает «affected» = всё
Affected + remote cacheТолько задачи без кэшированного выводасекунды–2 мин на тёплом кэшеСлишком широкий ключ → ложные промахи

Remote caching: второй множитель

Affected-определение говорит «не запускай нетронутые проекты». Remote caching говорит «даже не запускай тронутые проекты, если кто-то уже произвёл ровно этот вывод». Turborepo и Nx хэшируют каждый вход задачи — исходники, зависимости, env-переменные, конфиг задачи — и хранят вывод (артефакты сборки, результаты тестов, логи) под этим хэшем в общем кэше. Если коллега или твой прошлый прогон CI уже собрал этот хэш, задача — это попадание в кэш: вывод скачивается, а не пересчитывается, за миллисекунды.

Это и замыкает петлю между разработчиками и CI. Vercel замерил падение времени публикаций Next.js на 80% после того, как сборки SWC стали шариться через remote cache. Mercari отчитался примерно о 50%-м сокращении длительности Turbo-задач и 30%-м сокращении общей длительности джоба после подключения remote cache Turborepo к GitHub Actions. Частая цифра тёплого кэша — CI с ~6 минут до ~45 секунд. Cache hit rate — это метрика, за которой следишь: в репозитории на много пакетов, где большинство PR локальны, достижимы hit rate в 80–90%, и каждый процент hit rate — это минуты CI (и деньги), которые ты не тратишь.

Почему это работает

Ключ кэша решает всё. Если забыть включить вход — скажем, общий tsconfig или env-переменную, меняющую вывод, — получишь ложное попадание: кэш отдаёт устаревший артефакт и выкатывает баг. Если включить слишком много — волатильный файл, меняющийся каждый прогон, или слишком широкий glob, — получишь ложный промах: хэш никогда не повторяется, кэш бесполезен, а за хранение ты заплатил впустую. Тюнинг cache hit rate — это в основном тюнинг того, что считается входом.

Границы: останови хаб до того, как он сформируется

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

Nx делает это через теги и ESLint-правило @nx/enforce-module-boundaries. Ты тегируешь каждый проект (type:feature, type:ui, scope:checkout) и объявляешь depConstraints: feature может зависеть от ui, ui может зависеть только от util, ничто не может зависеть от feature. Импорты, нарушающие правило, валят линт — и то же правило ловит циклические зависимости («Circular dependency between A and B detected»), это другой способ превратить граф в спагетти. Turborepo опирается на границы пакетов и exports в package.json, чтобы публичная поверхность пакета была явной, и потребители не могли лезть в его внутренности. Ход сеньора — разбить god-пакет: вместо одного utils, который импортируют все, выпустить @acme/format-date, @acme/http, @acme/result, у каждого узкий fan-in, чтобы изменение в форматировании даты инвалидировало только горстку того, что реально форматирует даты.

Выбери лучший вариант

CI добрался до 40+ мин: каждый PR пересобирает репо, потому что все импортируют один @acme/utils. Выбери фикс, который бьёт в причину.

Монорепозиторий против полирепозитория, честно

Суперсила монорепозитория — атомарное изменение: переименуй функцию в общей библиотеке и обнови всех вызывающих в одном PR, с одним прогоном CI, доказывающим, что всё ещё компилируется. Никаких бампов версий, реестра, «выкати либу, подожди, бампни потребителей, выкати их». Цена реальна: тебе нужны инструменты, знающие граф (Nx/Turborepo/Bazel), remote caching и обеспеченные границы, иначе репо рушится под собственным CI. Полирепозиторий переворачивает это: каждый репо тривиально быстрый и независимый, но общий код едет через реестр, и ломающее изменение в общем пакете означает синхронные бампы, дрейф версий и раскопки «какой сервис на старой версии». Честная формулировка: монорепозиторий меняет более высокую сложность инструментов на меньшую цену координации; полирепозиторий меняет меньшую сложность инструментов на более высокую цену координации. Выбирай монорепозиторий, когда пакеты много шарят и меняются вместе; выбирай полирепозиторий, когда команды по-настоящему независимы и общаются только по API.

Викторина

В монорепозитории на 40 пакетов что на самом деле вычисляет «affected» (Nx) / «filtered» (Turborepo) для PR?

Викторина

Cache hit rate высокий, но один PR отдал устаревшую сборку, которая выкатила баг. Самая вероятная причина?

Расставь шаги по порядку

Расставь, как инструмент, знающий граф, решает, что запускать для PR:

  1. 1 Построить граф зависимостей из импортов + workspace-файлов package.json
  2. 2 Сделать diff PR с базовой веткой, чтобы найти напрямую изменённые проекты
  3. 3 Пройти по графу и добавить каждый проект, зависящий от изменённого (affected-набор)
  4. 4 Захэшировать входы каждой affected-задачи и проверить remote cache по этому хэшу
  5. 5 Запустить только задачи-промахи; скачать выводы для задач-попаданий
Вспомните перед уходом
  1. 01
    Объясни, почему один пакет «shared utils» может разогнать CI монорепозитория с 4 минут до 40, и что с этим делать.
  2. 02
    В чём разница между affected-определением и remote caching, и почему нужны оба?
Итог

Монорепозиторий быстр или медленен из-за одной вещи — графа зависимостей. Инструменты, знающие граф (Nx, Turborepo), разбирают импорты в направленный граф и используют его, чтобы вычислить affected-набор — изменённые проекты плюс всё, что от них зависит, — поэтому большинство PR собирают срез, а не мир, часто роняя репо на 40 пакетов с 30–45 минут до 1–5. Remote caching умножает это, хэшируя входы каждой задачи и скачивая сохранённые выводы при попадании, — так команды отчитываются о 50–80% сокращении CI и прогонах на тёплом кэше за секунды; подвох — в ключе кэша, где пропущенный вход даёт опасное ложное попадание, а слишком широкий — бесполезные ложные промахи. Ничто из этого не переживёт плохого графа: один хаб «shared utils» делает affected-набор всем репозиторием и промахивает кэш для каждого зависимого, поэтому дисциплина, которая реально держит монорепозиторий быстрым, — это обеспеченные границы модулей: теги Nx, правило enforce-module-boundaries и явные exports пакетов, которые разбивают god-пакеты и запрещают циклические и кросс-доменные импорты. Против полирепозитория сделка честна: монорепозиторий покупает атомарные кросс-пакетные изменения ценой инструментов, знающих граф, и дисциплины границ; полирепозиторий покупает тривиальную скорость на репо ценой версионирования через реестр и кросс-репозиторной координации.

Продолжить восхождение ↑Monorepo: обзор с выбором ответа
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.