Build tooling — почему esbuild/vite, а не webpack

Контекст: компилятор (ngtsc/linker, см. 00-overview) выдаёт JS-модули. Но браузеру нельзя отдать сотни модулей по одному — нужно собрать бандл: разрешить импорты, выкинуть мёртвый код, минифицировать, отдать dev-сервер с hot-reload. Этим занимается bundler. Это отдельный слой, НЕ компилятор шаблонов.

Что значит «builder» в Angular

В angular.json нашего demo21:

"builder": "@angular/build:application"   // prod-сборка (ng build)
"builder": "@angular/build:dev-server"    // ng serve
"builder": "@angular/build:unit-test"     // тесты (vitest)

Builder — это абстракция Angular CLI: «чем выполнять команду ng build». Внутри @angular/build:application спрятаны esbuild + vite. В v14 builder был @angular-devkit/build-angular:browser — внутри него сидел webpack.

Как менялся bundler по версиям

v14 v21
Builder @angular-devkit/build-angular @angular/build
Prod-бандл webpack esbuild (+ Rollup для финальной оптимизации)
Dev-сервер webpack-dev-server vite
Язык бандлера JS (Node) esbuild на Go
Тесты Karma + Jasmine vitest (на vite)

Почему ушли от webpack

webpack — мощный и гибкий, но:

  • Медленный. Написан на JS, делает много проходов, тяжёлый граф зависимостей. Холодный старт и пересборки больших приложений — десятки секунд.
  • Сложный конфиг. Лоадеры, плагины, ручная настройка — высокий порог.
  • Создавался до эпохи нативных ES-модулей в браузере; всё гонял через свой рантайм.

Почему именно esbuild + vite

Не «esbuild ИЛИ vite» — их комбинируют, потому что у них разные сильные стороны:

esbuild — бандлер/минификатор/транспайлер на Go:

  • в 10–100× быстрее webpack за счёт нативного кода и параллелизма;
  • делает transpile (TS→JS), bundling, tree-shaking, минификацию.
  • Минус: его API для code-splitting/оптимизации беднее, чем у зрелых бандлеров.

vite — dev-сервер нового поколения:

  • в dev не бандлит всё приложение. Браузеры умеют нативные ESM, поэтому vite отдаёт модули по запросу (lazy) и держит мгновенный HMR — поменял файл, обновился только он. Поэтому ng serve в v21 стартует и пересобирает почти мгновенно.
  • prebundle зависимостей делает тем же esbuild.

Rollup — для prod-сборки Angular добавляет финальный проход Rollup-подобной оптимизации поверх esbuild (лучший tree-shaking/chunking, чем у голого esbuild).

Итог разделения труда:

ng serve (dev)  → vite (ESM по запросу, HMR) + esbuild (prebundle)   → быстро итерировать
ng build (prod) → esbuild (transpile+bundle) + Rollup-оптимизация    → маленький бандл

Как компилятор Angular встроен в bundler

ngtsc и linker — это не часть webpack/esbuild, а плагины поверх них. В пайплайне @angular/build:

  1. TS + ngtsc компилируют твои @Component в full Ivy (см. 01-full-pipeline).
  2. Babel linker plugin дотягивает partial-либы из node_modules (см. 02-partial-ivy-and-linker).
  3. esbuild собирает результат в бандл, tree-shake выкидывает dev-метаданные (setClassMetadata, debugName), минифицирует.
  4. dev: vite отдаёт это с HMR; prod: Rollup-оптимизация + хеши файлов.

То есть «компиляция шаблона» и «сборка бандла» — два разных этапа в одном прогоне: сначала Angular-компилятор делает Ivy-код, потом bundler собирает его в main.js.

Как вообще выбирают bundler (общий принцип)

Не «какой моднее», а по задаче:

  • скорость итерации в dev (HMR, холодный старт) → vite;
  • скорость и простота prod-сборки → esbuild;
  • максимальный контроль над выходным бандлом (сложный code-splitting, легаси-форматы) → исторически webpack/Rollup;
  • экосистема/плагины под конкретный фреймворк → часто решает за тебя сам фреймворк (Angular CLI выбрал esbuild/vite, и вручную это менять обычно не нужно).

Angular спрятал выбор в builder: ты пишешь ng build, а связку esbuild+vite+Rollup поддерживает команда Angular. В v14 за тем же фасадом был webpack.

Итог

  1. Компилятор ≠ bundler. ngtsc/linker делают Ivy-код; bundler собирает бандл.
  2. v21 сменил webpack на esbuild (Go, быстро) + vite (dev/HMR) + Rollup (prod-опт.); тесты — vitest вместо karma.
  3. Мотив смены — скорость (esbuild на Go) и нативные ESM (vite не бандлит dev).
  4. Angular-компилятор встроен как плагины поверх bundler, а не наоборот.