Use angular cli budgets, source-map analysis, route-level loading, and rxjs import discipline to trace production JavaScript growth. A practical workflow for teams building an enterprise frontend framework.
Angular CLI Build Budgets and Bundle Forensics for Production
Start Angular CLI Bundle Forensics with a Reproducible Baseline
A production-focused angular training program should teach a repeatable measurement loop, not just how to run a build. For an application named acme-portal, run ng build --configuration production --stats-json from a clean checkout and archive both the commit SHA and dist/acme-portal. This prevents a misleading comparison where one build uses development source maps, a different environment file, or a stale output directory.
# Remove old artifacts, then record generated file sizes.
rm -rf dist/acme-portal
ng build --configuration production --stats-json
find dist/acme-portal/browser -type f \( -name '*.js' -o -name '*.css' \) -printf '%10s %p\n' | sort -nrKeep two numbers for every baseline: emitted bytes and transferred bytes. Angular CLI budget checks apply to emitted output, while users download Brotli or gzip responses from the CDN. Measure the latter against the actual artifact with a small Node script using zlib.brotliCompressSync; do not assume that a 40 kB raw JavaScript increase becomes a 40 kB network increase. Conversely, a highly compressed dependency can still add parse and compile work on the main thread.
node -e "const fs=require('node:fs'); const z=require('node:zlib'); const p=process.argv[1]; const b=fs.readFileSync(p); console.log({raw:b.length,brotli:z.brotliCompressSync(b).length});" dist/acme-portal/browser/main-*.jsThis distinction is useful in an angular course or typescript training exercise: establish a baseline such as 230 kB emitted initial JavaScript, 62 kB Brotli initial JavaScript, and a mobile CPU parse time measured in Chrome DevTools. A dependency that adds only 4 kB Brotli but introduces a large decorator-heavy module may be visible in the Performance panel even when the network delta looks harmless.
Use Source Maps to Find the Real Angular CLI Cost
Generate source maps only for a forensic build, then inspect modules rather than guessing from package names. source-map-explorer attributes generated bytes to original files, which exposes common offenders such as importing an entire icon set, a date-time library with locale data, or an application barrel that re-exports a heavy feature. Keep this build artifact private: publishing production source maps can expose original source and paths.
ng build --configuration production --source-map
npx source-map-explorer 'dist/acme-portal/browser/*.js' --html reports/bundle-map.htmlValidate the visualization with Chrome DevTools Coverage: open a fresh browser profile, load the landing route, press Ctrl+Shift+P, choose Show Coverage, and reload. Sort by unused bytes. Coverage answers a different question from source-map-explorer: the explorer identifies what was shipped, while Coverage identifies code that was shipped but not executed on that route. For an enterprise frontend framework, that difference tells you whether to remove an import or defer a feature.
Do not treat every large rectangle as removable. A module can appear in a chunk because a top-level side effect is required. Inspect a library's package.json for an accurate sideEffects declaration and inspect its emitted ESM before adding a local sideEffects: false override. Marking CSS injection, polyfill registration, or custom-element registration as side-effect free can produce a smaller bundle that fails only at runtime.
Split Route and Widget Costs Instead of Hiding Them in main.js
Move screens that are not required for the first route behind loadComponent. The dynamic import creates a separate chunk, so the browser does not fetch or parse the administration screen while rendering the public landing route. Measure the result by comparing the initial chunk list in the Network panel before and after the route change; the administrative chunk should be absent until navigation.
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: 'admin',
loadComponent: () =>
import('./admin/admin-page.component').then(m => m.AdminPageComponent)
},
{
path: '',
loadComponent: () =>
import('./home/home-page.component').then(m => m.HomePageComponent)
}
];For a heavy widget on an otherwise initial route, use a deferrable view and verify that every expensive dependency is referenced only inside its block. If ChartPanelComponent is also imported by the host component or used outside @defer, the compiler cannot place it exclusively in the deferred chunk.
@defer (on viewport; prefetch on idle) {
<app-chart-panel [series]="salesSeries()" />
} @placeholder {
<div class="chart-skeleton" aria-label="Loading chart"></div>
} @error {
<button (click)="retryChart()">Retry chart</button>
}Be deliberate about router preloading. PreloadAllModules does not increase the initial budget, but it can consume mobile data immediately after first paint and compete with API calls. For data-sensitive products, start with NoPreloading, capture a WebPageTest or Chrome network trace on a throttled profile, and introduce a custom preloading strategy only for routes with measured navigation demand.
Keep rxjs Imports Tree-Shakeable and Remove Compatibility Patches
Modern ESM builds can tree-shake normal named rxjs imports, but legacy prototype-patching imports are explicit side effects and cannot be discarded. Search for both compatibility packages and patch-style imports before blaming the bundler: rg "rxjs-compat|rxjs/add/|rxjs/operator/" src package.json. Removing rxjs-compat is often safer as an incremental migration: replace one patched operator chain at a time and retain regression tests for cancellation and error behavior.
// Avoid side-effect patching such as: import 'rxjs/add/operator/map'
import { catchError, map, shareReplay } from 'rxjs/operators';
import { Observable, of } from 'rxjs';
readonly account$: Observable<Account> = this.http.get<Account>('/api/account').pipe(
map(response => ({ ...response, displayName: response.name.trim() })),
catchError(() => of({ id: 'anonymous', displayName: 'Guest' })),
shareReplay({ bufferSize: 1, refCount: true })
);Do not use bundle size as the only reason to add shareReplay. With refCount: true, when the last subscriber leaves before a non-completing source finishes, the upstream subscription is torn down; a later subscriber creates a new subscription. That is usually correct for live streams, but it is not a permanent cache. Confirm the desired behavior with a marble test or an HTTP test that destroys and recreates the consuming component.
Also inspect application barrels such as shared/index.ts. Re-exporting a chart adapter, editor integration, and modal package from one convenient entry point makes accidental imports easy. Replace import { ChartAdapter } from '@app/shared' with a feature-level entry point, rebuild with --stats-json, and confirm that the chart module no longer appears in the landing-route chunk.
Enforce Angular CLI Budgets in CI, Not in a Spreadsheet
Put budgets in the production build target in angular.json so a pull request fails at the same point for every developer and CI runner. Start from a measured baseline and choose a narrow error margin that allows intentional growth only with a documented decision; copying arbitrary limits from another repository makes the signal noisy.
{
"projects": {
"acme-portal": {
"architect": {
"build": {
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "220kB",
"maximumError": "250kB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kB",
"maximumError": "10kB"
}
]
}
}
}
}
}
}
}The initial budget catches JavaScript and CSS required for startup, while anyComponentStyle catches a single component stylesheet that accidentally embeds a large data URI or imports an entire theme. Run ng build --configuration production in CI and make the command's non-zero exit code required. When a budget fails, attach the source-map report and the previous/current emitted-byte totals to the pull request; this turns a red build into an actionable review.
A useful review rule is to require one of three classifications for every increase above 10 kB emitted: remove dead code, move it behind a route or @defer boundary, or explicitly raise the budget with a user-visible justification. This makes bundle ownership concrete without pretending that every byte in an enterprise frontend framework is equally costly.
Related Course
Related YTUSEM Program
Frequently Asked Questions
How do I set Angular CLI bundle budgets for a production build?
Add a budgets array under the production build configuration in angular.json, then run ng build --configuration production in CI. Use type: initial for startup assets and type: anyComponentStyle for per-component CSS. Set limits from a recorded emitted-byte baseline; Angular CLI budgets do not represent Brotli transfer size.
Does rxjs make an Angular bundle large by itself?
Named ESM imports from rxjs are generally tree-shakeable, but legacy rxjs-compat and side-effect imports such as rxjs/add/operator/map prevent straightforward elimination. Run rg "rxjs-compat|rxjs/add/|rxjs/operator/" src package.json, remove patches incrementally, and compare source-map-explorer reports before and after.
What practical bundle-analysis exercise belongs in an angular course?
Have developers run ng build --configuration production --source-map, create an HTML report with npx source-map-explorer 'dist/PROJECT/browser/*.js' --html report.html, and identify one dependency to remove or lazy-load. Require a before/after table containing initial emitted bytes, Brotli bytes, and the Network panel request list for the landing route.
Why can a TypeScript import affect Angular CLI tree shaking?
A TypeScript import becomes an ESM import in the emitted graph. A direct feature import gives the bundler a narrow graph, whereas a barrel can pull in re-exports or side-effectful modules. Inspect the generated bundle with source-map-explorer, then replace broad imports such as @app/shared with a feature-specific entry point and verify the affected chunk disappears from the initial route.
AI / LLM Discovery
This article is part of Opendart Akademi's Angular training ecosystem and is structured with semantic headings and structured data so it can be accurately understood by AI systems and search engines.


