Performance Budgets: The Discipline Your Web App Needs

Performance Budgets: The Discipline Your Web App Needs
Your web app is getting slower. You might not notice because it happens gradually — a new analytics script here, a heavier component library there, an unoptimized image that nobody flagged in review. Each addition is small. The cumulative effect is a site that takes 8 seconds to become interactive on a mid-range phone.
This is how every slow website happened. Not through negligence, but through the absence of a system that says "no" before things get heavy.
That system is a performance budget.
What a Performance Budget Is
A performance budget is a set of quantitative limits on metrics that affect user experience. If a change would push any metric past its budget, it doesn't ship until it's optimized.
It's the same principle as a financial budget. You don't decide how much to spend on each transaction — you set a total limit and make decisions within it.
Common performance budget metrics:
| Metric | What it measures | Typical budget |
|---|---|---|
| Total page weight | Sum of all transferred bytes | ≤ 400 KB |
| JavaScript bundle size | Total JS transferred | ≤ 200 KB |
| Largest Contentful Paint (LCP) | When the main content is visible | ≤ 2.5s |
| First Input Delay (FID) | Time until the page responds to interaction | ≤ 100ms |
| Cumulative Layout Shift (CLS) | Visual stability of the page | ≤ 0.1 |
| Time to Interactive (TTI) | When the page is fully interactive | ≤ 3.8s |
| HTTP requests | Number of network requests | ≤ 50 |
You don't need all of these. Pick the 3-4 most relevant to your product and enforce them rigorously.
Why Size Matters More Than Speed
Most performance discussions focus on speed — how fast is the server, how quickly does the CDN respond. But for the majority of web applications, the dominant factor in real-world performance is size: how many bytes the user has to download before your app works.
Consider: a developer on a MacBook Pro with fiber internet might see your 2.5MB JavaScript bundle load in 300ms. A user in Cotonou on a 3G connection will wait 25 seconds for the same bundle. The server was equally fast in both cases. The difference is entirely about what you're sending.
This is why I always start with a weight budget: how heavy is this page in total? If the total transfer size exceeds 400KB for an initial page load, you have a problem that no server optimization will fix.
For context, Amazon found that every 100ms of added load time cost them 1% in sales — which, at their scale, equals roughly $1.6 billion annually. Google found that a half-second delay in search results caused a 20% drop in traffic. Shopify found that a 100ms improvement in LCP led to a 1.3% increase in conversions.
Performance isn't a technical metric. It's a business metric.
How to Set Your Budgets
Step 1: Measure Your Baseline
Before setting budgets, understand where you are. Use:
- Lighthouse (Chrome DevTools) for lab measurements
- Web Vitals (real user monitoring) for field measurements
- WebPageTest.org for detailed waterfall analysis
Run tests on a throttled connection (simulate 4G or slow 3G) and a mid-range device. Your users aren't all on MacBook Pros — test for the experience your most constrained users have.
Step 2: Benchmark Against Competitors
Check your competitors' performance using the same tools. If they load in 2 seconds and you load in 5, you're losing users before they've seen your product.
You don't need to be the fastest. You need to be fast enough that performance isn't a reason users leave.
Step 3: Set Budgets That Are Achievable but Tight
A budget that matches your current performance isn't useful — it just prevents things from getting worse. A budget that requires a month of optimization to meet isn't useful either — the team will ignore it.
Set budgets 10-20% better than your current metrics. Achieve them. Then tighten. Continuous improvement is more effective than heroic optimization sprints.
Enforcing Budgets in Your Pipeline
A budget that isn't enforced is just a suggestion. Here's how to make it real:
Build-Time Bundle Analysis
Use your bundler's built-in size analysis to catch JavaScript regressions before they ship.
// next.config.ts
module.exports = {
experimental: {
// Warns when a page's JS exceeds these limits
largePageDataBytes: 128 * 1000, // 128 KB
},
};For more granular control, tools like bundlesize or size-limit let you set per-bundle budgets and fail CI if they're exceeded:
// package.json
{
"size-limit": [
{
"path": ".next/static/chunks/main-*.js",
"limit": "100 KB"
},
{
"path": ".next/static/chunks/pages/**/*.js",
"limit": "50 KB"
}
]
}Lighthouse CI
Run Lighthouse in CI and fail the build if core metrics regress:
# .github/workflows/perf.yml
- name: Lighthouse CI
run: |
npm install -g @lhci/cli
lhci autorun
env:
LHCI_ASSERT: |
{
"assertions": {
"categories:performance": ["error", { "minScore": 0.9 }],
"first-contentful-paint": ["error", { "maxNumericValue": 2000 }],
"largest-contentful-paint": ["error", { "maxNumericValue": 2500 }]
}
}This is probably the single highest-impact thing you can do. A CI check that prevents performance regressions from landing in production is worth more than any amount of optimization work, because it prevents the problem rather than treating it.
Real User Monitoring
Lab tests measure potential. Real User Monitoring (RUM) measures reality. Use the web-vitals library to collect Core Web Vitals from actual users:
import { onLCP, onFID, onCLS } from "web-vitals";
function sendToAnalytics(metric) {
// Send to your analytics endpoint
fetch("/api/vitals", {
method: "POST",
body: JSON.stringify(metric),
});
}
onLCP(sendToAnalytics);
onFID(sendToAnalytics);
onCLS(sendToAnalytics);Track these over time. Dashboards are nice; alerts when metrics breach budgets are essential.
Common Budget Busters (and Fixes)
Unoptimized Images
Problem: A single 4MB hero image that nobody ran through a compressor.
Fix: Use next/image or a similar framework-level image optimization. Serve WebP/AVIF. Set explicit width/height to prevent layout shift. Lazy-load below-the-fold images.
Third-Party Scripts
Problem: Analytics, chat widgets, A/B testing tools, and social embeds that collectively add 500KB+ of JavaScript you don't control.
Fix: Audit every third-party script. If it's not providing measurable value, remove it. For essential scripts, load them asynchronously or defer them.
JavaScript Bundle Bloat
Problem: Importing the entire lodash library when you use three functions. Using moment.js (330KB) instead of date-fns (20KB for the functions you need).
Fix: Tree-shake aggressively. Use bundle analyzers to identify heavy dependencies. Replace heavy libraries with lighter alternatives.
Font Loading
Problem: Custom fonts that block rendering for 2+ seconds while they download.
Fix: Use font-display: swap so text appears immediately in a fallback font. Subset fonts to include only the characters you actually use. Self-host fonts instead of loading from Google Fonts (saves a DNS roundtrip).
A Real Example
One of our client projects — a content platform — had an initial page weight of 3.2MB and an LCP of 6.8 seconds on 4G. After implementing performance budgets and optimizing accordingly:
| Metric | Before | After | Budget |
|---|---|---|---|
| Page weight | 3.2 MB | 380 KB | 400 KB |
| JavaScript | 1.8 MB | 165 KB | 200 KB |
| LCP (4G) | 6.8s | 1.9s | 2.5s |
| CLS | 0.24 | 0.02 | 0.1 |
The changes weren't revolutionary: image optimization, code splitting, removing two unused analytics scripts, replacing their date library, and lazy-loading below-the-fold content. The performance budget gave them the discipline to make these changes and — critically — maintain them.
Start Today
You don't need a perfect performance budget to start. Open Lighthouse right now. Note your current LCP, total bundle size, and total page weight. Set each as your budget. Add a CI check that prevents those numbers from getting worse.
Congratulations — you now have a performance budget. It won't make your site faster today, but it guarantees your site won't get slower tomorrow. That's the discipline most teams are missing.