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:
- TS + ngtsc компилируют твои
@Componentв full Ivy (см. 01-full-pipeline). - Babel linker plugin дотягивает partial-либы из
node_modules(см. 02-partial-ivy-and-linker). - esbuild собирает результат в бандл, tree-shake выкидывает dev-метаданные
(
setClassMetadata,debugName), минифицирует. - 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.
Итог
- Компилятор ≠ bundler. ngtsc/linker делают Ivy-код; bundler собирает бандл.
- v21 сменил webpack на esbuild (Go, быстро) + vite (dev/HMR) + Rollup (prod-опт.); тесты — vitest вместо karma.
- Мотив смены — скорость (esbuild на Go) и нативные ESM (vite не бандлит dev).
- Angular-компилятор встроен как плагины поверх bundler, а не наоборот.