Building fast Angular apps is one thing. Keeping them fast as the codebase grows and many developers contribute is another. Without guardrails, large bundles slip into production: third-party libraries creep in, lazy loading is forgotten, and a single careless import can add hundreds of kilobytes to a critical route. The result: slower first paint, worse Core Web Vitals, and frustrated users.
In this article I’ll show a practical, production-ready approach to an Angular Performance Budget System that automatically prevents heavy bundles from being built or deployed. The goal is twofold:
Build-time enforcement — fail builds or block merges when budgets are breached.
Runtime safeguards — detect and mitigate heavy bundles that still reach clients.
I’ll cover design, concrete Angular and Node examples, CI integration (GitHub Actions), monitoring, and recommended budgets. The article is written in simple Indian English and aimed at senior developers who want a pragmatic, repeatable solution.
Table of contents
Why a performance budget system matters
What to budget (metrics and assets)
Angular CLI built-in budgets (quick wins)
Build-time enforcement: advanced checks and custom scripts
Integrating budgets into CI/CD (example: GitHub Actions)
Runtime safeguards and progressive rollout
Monitoring, alerts, and rollback strategies
Developer workflow and education
Performance budget thresholds — practical recommendations
Caveats and limitations
Conclusion
1. Why a performance budget system matters
A performance budget is an explicit limit on a resource (e.g., JS bundle size, image weight, number of fonts) meant to keep your app fast. Enforcing budgets is important because:
Teams scale: more code, more dependencies, and more accidental regressions.
Shipping large bundles hurts users on slow networks (mobile, 2G/3G) common in many regions.
Using budgets as blocking rules turns performance from an afterthought into a continuous quality gate.
We will implement both automated build blockers and runtime guards so performance is not just tested — it is enforced.
2. What to budget (metrics and assets)
A budget system should track both network and render concerns:
Main JS bundle (critical path).
Lazy chunks (per route).
Vendor bundle (third-party libs).
Initial download size (all assets required for first render).
CSS size (critical CSS).
Fonts (woff2 weight).
Images (initial hero images).
Number of requests (HTTP/2 connection limits matter).
Runtime metrics (largest contentful paint LCP, FCP).
Start with bundle sizes and images; add runtime metrics later using Lighthouse or Sentry.
3. Angular CLI built-in budgets (quick wins)
Angular CLI has a simple budgets feature you can add to angular.json. This is the fastest way to block a build when a size is exceeded.
Example angular.json snippet
"projects": {
"my-app": {
"architect": {
"build": {
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "250kb",
"maximumError": "500kb"
},
{
"type": "bundle",
"name": "main",
"maximumWarning": "150kb",
"maximumError": "300kb"
},
{
"type": "bundle",
"name": "vendor",
"maximumWarning": "200kb",
"maximumError": "400kb"
}
]
}
}
}
}
}
}
maximumWarningwill surface a warning.maximumErrormakes the build fail (non-zero exit).Types:
initial,bundle,anyComponentStyle,allScript,all.
Pros: simple and built into Angular CLI.
Cons: coarse control; does not cover per-route chunks easily; no image/font checks.
4. Build-time enforcement: advanced checks and custom scripts
Angular CLI budgets are great, but for a comprehensive policy you need custom checks:
Per-chunk budgets (for route chunks).
Vendor submodule checks (e.g., large package introduced).
Image/font weight checks.
Source map size checks (source maps add server cost).
Custom regression checks (bundle increased by > X% vs baseline).
4.1 Generate stats.json from Angular build
First generate a stats.json (webpack stats) to inspect bundles:
ng build --configuration production --stats-json
# produces dist/stats.json
4.2 Node script to parse stats.json and block build
Create scripts/check-budgets.js:
const fs = require('fs');
const path = require('path');
const statsPath = path.resolve(__dirname, '../dist/stats.json');
if (!fs.existsSync(statsPath)) {
console.error('stats.json not found. Run ng build --stats-json first.');
process.exit(1);
}
const stats = JSON.parse(fs.readFileSync(statsPath, 'utf8'));
// Example budgets: main <= 300 KB, vendor <= 500 KB, any chunk <= 200 KB
const KB = 1024;
const budgets = {
main: 300 * KB,
vendor: 500 * KB,
chunk: 200 * KB,
};
// Gather assets
const assets = stats.assets || [];
let failed = false;
for (const asset of assets) {
const name = asset.name;
const size = asset.size;
// match main.*.js
if (/^main.*\.js$/.test(name) && size > budgets.main) {
console.error(`Budget exceeded: ${name} is ${(size/KB).toFixed(1)} KB, limit ${(budgets.main/KB)} KB`);
failed = true;
}
// match vendor.*
if (/^vendor.*\.js$/.test(name) && size > budgets.vendor) {
console.error(`Budget exceeded: ${name} is ${(size/KB).toFixed(1)} KB, limit ${(budgets.vendor/KB)} KB`);
failed = true;
}
// other chunk files
if (/\.js$/.test(name) && !/^main.*|vendor.*|polyfills.*|runtime.*$/.test(name) && size > budgets.chunk) {
console.error(`Budget exceeded: chunk ${name} is ${(size/KB).toFixed(1)} KB, limit ${(budgets.chunk/KB)} KB`);
failed = true;
}
}
if (failed) process.exit(2);
console.log('All budgets OK.');

Join the conversation! Your thoughts help the community grow.