Оптимизация работы с большими монорепозиториями

Большие монорепозитории в JavaScript обычно состоят из множества пакетов, объединённых единым управлением зависимостями и общими инструментами сборки. Типичная структура включает:

  • набор библиотек (packages/*)
  • несколько приложений (apps/*)
  • общие утилиты (shared/*)
  • единый node_modules через workspace-менеджер

В такой среде основная проблема сборки заключается не в единичной компиляции, а в экспоненциальном росте графа зависимостей. Даже небольшое изменение в одном пакете может инициировать пересборку десятков модулей.

Esbuild в этом контексте используется как высокоскоростной компилятор и бандлер, способный значительно сократить время сборки, но только при правильной организации графа модулей и кэширования.


Принципы работы Esbuild в монорепозитории

Esbuild строит граф зависимостей начиная с entry-point и рекурсивно обходит импортируемые модули. В монорепозитории это означает:

  • каждый пакет может стать частью единого графа
  • отсутствие строгой изоляции приводит к повторной обработке одинаковых зависимостей
  • неправильная настройка alias и external увеличивает размер бандла и время сборки

Ключевая особенность: Esbuild не управляет монорепозиторием как системой, он работает только с графом импортов.


Разделение пакетов и контроль границ

Эффективная работа начинается с чёткого разделения пакетов.

Изоляция через workspace

Использование Yarn Workspaces, pnpm или npm workspaces создаёт единое дерево зависимостей, но важно не полагаться на это автоматически.

Критично:

  • каждый пакет должен иметь собственный tsconfig.json
  • каждый пакет должен иметь явную точку входа (src/index.ts)
  • запрещено неявное импортирование внутренних файлов других пакетов без публичного API

Настройка внешних зависимостей (external)

Одна из главных причин деградации производительности — попадание внутренних или внешних зависимостей в бандл.

Esbuild позволяет явно исключать зависимости:

external: [
  'react',
  'react-dom',
  '@company/utils'
]

В монорепозитории особенно важно:

  • исключать все node_modules
  • исключать локальные workspace-пакеты, если они публикуются отдельно
  • предотвращать дублирование React и runtime-библиотек

Критический момент: если не пометить workspace-пакеты как external, Esbuild может собрать их несколько раз в разных частях графа.


Управление графом зависимостей между пакетами

Монорепозиторий формирует сложный DAG (directed acyclic graph), где пакеты зависят друг от друга.

Оптимальная стратегия:

  • библиотечные пакеты компилируются отдельно в dist
  • приложения используют уже скомпилированные артефакты
  • внутренние импорты идут через package.json exports

Пример структуры:

packages/
  ui/
  utils/
apps/
  web/

Каждый пакет:

  • имеет собственный build через esbuild
  • не импортирует исходники других пакетов напрямую (кроме dev-режима)

Использование incremental build и watch mode

Esbuild поддерживает режим наблюдения, который критичен для монорепозиториев.

esbuild.build({
  entryPoints: ['src/index.ts'],
  bundle: true,
  outdir: 'dist',
  watch: true
})

Но в больших репозиториях этого недостаточно.

Оптимизация достигается через:

  • разделение build-ранов по пакетам
  • запуск отдельных процессов esbuild на каждый пакет
  • использование общего кеша файловой системы

Кэширование и снижение повторных вычислений

Esbuild сам по себе не имеет полноценного долговременного дискового кеша, поэтому в монорепозитории используются дополнительные стратегии:

1. Разделение сборок

Каждый пакет собирается отдельно:

  • уменьшение области пересборки
  • изоляция изменений

2. Внешний кеш через инструменты оркестрации

Используются task-runner’ы (Turborepo, Nx), которые кэшируют результаты:

  • входные файлы
  • hash зависимостей
  • output dist

3. Стабильные пути и структура

Кеш ломается при:

  • нестабильных алиасах
  • динамических import() без контроля
  • изменении порядка зависимостей

Работа с TypeScript в монорепозитории

Esbuild компилирует TypeScript без type-checking, что создаёт важное разделение ответственности:

  • Esbuild: трансформация и бандлинг
  • tsc: проверка типов

В монорепозитории это разделяется:

  • отдельный процесс tsc --noEmit
  • Esbuild используется только для production bundling

Оптимизация достигается через:

  • включение incremental: true в TypeScript
  • использование project references (references в tsconfig)

Project References и ускорение сборки

TypeScript project references позволяют разбить монорепозиторий на граф компиляции:

packages/utils -> packages/ui -> apps/web

Преимущества:

  • компиляция только изменённых пакетов
  • согласованность с Esbuild графом
  • минимизация повторной обработки типов

Path aliases и их влияние на скорость

Часто используются paths в tsconfig:

{
  "paths": {
    "@utils/*": ["packages/utils/src/*"]
  }
}

Проблема:

  • Esbuild не всегда автоматически синхронизирует alias с TypeScript
  • неправильная настройка приводит к дублированию модулей

Решение:

  • использовать esbuild-plugin-tsconfig-paths
  • или ручной alias в esbuild config

Параллельная сборка пакетов

Одно из ключевых преимуществ монорепозитория — возможность параллелизации.

Стратегия:

  • каждый пакет запускает свой esbuild процесс
  • используется worker pool или task runner
  • ограничивается число параллельных процессов CPU cores

Пример архитектуры:

  • process 1 → packages/ui
  • process 2 → packages/utils
  • process 3 → apps/web

Минимизация графа зависимостей

Чем меньше граф — тем быстрее сборка.

Практики:

  • удаление баррельных импортов (index.ts с re-export всех модулей)
  • замена deep-imports на прямые зависимости
  • разделение больших пакетов на подмодули

Оптимизация bundle splitting

Esbuild поддерживает code splitting:

splitting: true,
format: 'esm'

В монорепозитории это позволяет:

  • разделять vendor-код
  • избегать повторного включения библиотек
  • ускорять инкрементальные пересборки

Важно:

  • code splitting работает только с ESM
  • неправильная конфигурация приводит к дублированию runtime

Метаданные сборки и анализ узких мест

Esbuild умеет генерировать метаданные:

metafile: true

Это позволяет:

  • анализировать размер модулей
  • выявлять тяжёлые зависимости
  • отслеживать дублирование пакетов

Типичный анализ включает:

  • входные точки
  • дерево импортов
  • размер каждого модуля

На основе метафайла строятся оптимизации:

  • удаление лишних зависимостей
  • рефакторинг крупных модулей
  • перенос shared кода в отдельный пакет

Изоляция dev и production сборок

В монорепозитории важно разделять режимы:

  • dev: быстрые инкрементальные сборки
  • prod: оптимизированный бандл

Рекомендации:

  • отключать minify в dev
  • использовать watch только на уровне пакетов
  • избегать глобального esbuild процесса для всего репозитория

Типичные проблемы больших монорепозиториев

Дублирование зависимостей

Возникает при:

  • отсутствии external
  • неправильных alias
  • прямом импорте исходников пакетов

Пересборка всего графа

Причины:

  • общий entry point
  • отсутствие разделения пакетов
  • единый build script для всех

Замедление watch mode

Причины:

  • слишком большой граф файлов
  • отсутствие ignore patterns
  • глубокие рекурсивные зависимости

Стратегия масштабирования сборки

При росте монорепозитория ключевым становится переход к многоуровневой архитектуре:

  • уровень 1: независимые пакеты (utils, ui)
  • уровень 2: агрегирующие пакеты (design system)
  • уровень 3: приложения

Esbuild используется на каждом уровне отдельно, без попытки собрать всё единым процессом.

Основной принцип: сборка должна следовать структуре репозитория, а не наоборот.