Building for Production
Before an Angular app reaches users it must be compiled, tree-shaken, minified, and bundled into static assets that a browser can load fast. The ng build command handles all of this, and on modern Angular (17+) it defaults to a production-optimized build powered by the esbuild-based application builder. Understanding what the build produces — and how to tune it — is the difference between a snappy app and a multi-megabyte download.
Running a production build
In Angular 17 and later, ng build runs in production mode by default. There is no longer a separate --prod flag — it was removed because production is the sensible default.
ng build
This compiles your app using the production configuration defined in angular.json, applies ahead-of-time (AOT) compilation, minifies the output, removes dead code, and writes everything to the output directory.
Output:
Initial chunk files | Names | Raw size | Transfer size
main-7XQ2K4UJ.js | main | 142.06 kB | 41.21 kB
polyfills-FFHX3O3N.js | polyfills | 34.52 kB | 11.28 kB
styles-5INURTSO.css | styles | 1.92 kB | 456 bytes
| Initial total | 178.50 kB | 52.94 kB
Application bundle generation complete. [3.214 seconds]
To build a specific named configuration explicitly, use --configuration:
ng build --configuration production
ng build --configuration staging
Use
ng buildto generate deployable artifacts. Useng serveonly for local development — its in-memory dev server is never meant for production traffic.
The dist output structure
By default the build writes to dist/<project-name>/. With the application builder, the output is split into a browser/ folder (and a server/ folder if you enable SSR):
dist/my-app/
├── browser/
│ ├── index.html
│ ├── main-7XQ2K4UJ.js
│ ├── polyfills-FFHX3O3N.js
│ ├── styles-5INURTSO.css
│ ├── chunk-AB12CD34.js
│ ├── favicon.ico
│ └── assets/
└── 3rdpartylicenses.txt
The browser/ directory is what you deploy to a static host or CDN. Lazy-loaded routes produce additional hashed chunk-*.js files that load on demand.
Output hashing and cache busting
Notice the hashes in filenames like main-7XQ2K4UJ.js. These content hashes change whenever the file’s contents change, which lets you set long-lived cache headers (Cache-Control: max-age=31536000, immutable) without ever serving stale code. When you deploy a new version, filenames change and browsers fetch the fresh files automatically.
Hashing is controlled by the outputHashing option:
| Value | Hashes applied | Typical use |
|---|---|---|
all | JS bundles and CSS (default for prod) | Production |
media | Referenced assets like images/fonts only | Rare |
bundles | JS and CSS bundles only | Rare |
none | No hashing | Local testing |
ng build --output-hashing=all
Because index.html is never hashed (it is the entry point), the browser always fetches the latest index.html, which in turn references the newly hashed bundles.
Optimization flags and configuration
Production optimizations are declared per-configuration in angular.json. A typical production configuration looks like this:
{
"configurations": {
"production": {
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "4kb",
"maximumError": "8kb"
}
]
}
}
}
Key options:
| Option | Effect |
|---|---|
optimization | Enables minification, tree-shaking, and CSS optimization |
sourceMap | Emits .map files for debugging; usually false in production |
extractLicenses | Collects third-party licenses into 3rdpartylicenses.txt |
budgets | Fails or warns the build when bundles exceed size thresholds |
outputPath | Overrides the default dist/<project-name> directory |
You can override any of these from the command line:
ng build --source-map --output-path=dist/preview
Bundle budgets
Budgets are a guardrail against bundle bloat. When a bundle crosses maximumWarning, the build prints a warning; crossing maximumError fails the build entirely. This is invaluable in CI to catch an accidentally imported heavy library before it ships.
Output:
▲ [WARNING] bundle initial exceeded maximum budget.
Budget 500.00 kB was not met by 78.40 kB with a total of 578.40 kB. [plugin angular-cli]
Analyzing the bundle
When a bundle is larger than expected, generate stats and inspect what is inside. The application builder supports --stats-json:
ng build --stats-json
npx esbuild-visualizer --metadata dist/my-app/stats.json --open
This produces a treemap showing exactly which modules and dependencies contribute to each chunk, so you can spot a stray moment or duplicated library and replace or lazy-load it.
Best Practices
- Keep
sourceMap: falsefor the public production bundle, but consider hidden source maps for error-monitoring tools so stack traces remain readable without exposing source to users. - Always enable
outputHashing: "all"and pair it withimmutable, long-lived cache headers on hashed files while leavingindex.htmluncached. - Define and enforce bundle
budgetsso size regressions fail CI rather than surprising you in production. - Lazy-load feature routes with
loadComponent/loadChildrenso the initial bundle stays small and routes load on demand. - Run
ng build --stats-jsonperiodically and audit the treemap to catch accidental heavy imports early. - Deploy only the
browser/directory (plusserver/for SSR) — never ship source files, source maps, ornode_modules.