Skip to main content

6 min read #angular #esbuild #vite #build #tooling #frontend

One builder line and my rebuilds stopped being coffee breaks

Migrating from the webpack builder to Angular's esbuild-based application builder is one schematic and a few honest casualties. What broke, what got faster, and why the custom-webpack crowd should still jump.

There's a category of performance work where you profile, hypothesize, measure, and claw back milliseconds. And then there's the other kind: someone else rewrote the engine, and you just have to move over. The esbuild-based application builder is the second kind. New Angular apps have used it by default since v17; the interesting story is migrating an old app that grew up on webpack.

The headline from the app I migrated — five years old, thick with legacy — is blunt: cold production builds went from "start it and go do something else" to under a minute, and the dev server's incremental rebuilds dropped from double-digit seconds to fast enough that I stopped alt-tabbing. I did not optimize anything. I changed which tool builds the app.

The migration is one command

ng update @angular/cli --name=use-application-builder

The schematic rewrites angular.json from browser to application, flips main to browser, adjusts output paths, and leaves the rest of your configuration alone. Most of what you already declare transfers untouched: fileReplacements, the styles and scripts arrays, assets, size budgets. If your app is plain — no custom builders, no webpack fiddling — there's a decent chance you run the schematic, run ng serve, and you're done. Mine was not plain.

Casualty #1: the custom webpack config

If you're on ngx-build-plus or @angular-builders/custom-webpack, this is the part that stings: there is no "custom esbuild config" escape hatch of the same shape. Each thing your webpack config did needs its own esbuild-era answer. Going through mine turned out to be an archaeology dig — and most of the artifacts deserved burial:

  • A DefinePlugin injecting build-time constants — replaced by the builder's define option. One-to-one.
  • A loader shim for a library that assumed Node globals — the library had shipped a browser build two majors ago. Deleted the shim, updated the import.
  • A bundle-analyzer hook — ng build --stats-json plus esbuild-visualizer covers it.

The lesson generalizes: a years-old webpack config is mostly fossilized workarounds. Treat the migration as a chance to re-justify each line, not to port it.

Casualty #2: CommonJS gets loud

The new builder outputs ESM and is much less patient with CommonJS dependencies. Every CJS import now triggers a warning, and the old reflex — add it to allowedCommonJsDependencies and move on — costs you real money here, because CJS modules defeat tree-shaking. The warnings are a to-do list, and working through it is where the bundle wins hide: swapping one dinosaur date library for its ESM successor cut more from my main bundle than any lazy-loading work I'd done that quarter. Relatedly: if any app code still calls require() directly, it's over — output is ESM, imports only.

What you get beyond speed

The builder isn't just esbuild bolted on. Dev serve runs through Vite, which is where the instant rebuilds come from. SSR stopped being a parallel universe: the same application builder handles the server bundle and prerendering via the server and prerender options, instead of the old Universal two-builder contraption. And build output is properly hashed and code-split without the tuning folklore webpack accumulated.

The order of operations that worked

One process note, learned the annoying way: don't migrate the builder and update Angular majors in the same branch. First get the app green on a current Angular with the old builder — that isolates framework breakage from build breakage. Then flip the builder, with zero other changes, so every new error is unambiguously the builder's. Then burn down the CommonJS warnings at leisure. Three boring PRs beat one heroic one.

Verdict: this is the rare migration with an asymmetric payoff — a day or two of tooling archaeology, repaid every single time anyone on the team saves the file. If the only thing holding you back is a custom webpack config, that config is probably the strongest argument for going.