How to Set a Realistic Performance Budget for Your Site
A performance budget for a website is a set of hard numeric limits on the things that make a page slow, agreed before the work starts and checked automatically afterwards. Pick a handful of limits you can actually measure in a terminal, wire them into the build, and treat a breach the same way you would treat a failing test.
The temptation is to write something aspirational in a wiki page and never look at it again. That fails for the same reason most style guides fail: nothing enforces it. A budget only changes behaviour when it is a gate. The rest of this article is about which numbers to pick, where they come from, and how to make the gate real.
Pick limits you can measure from a shell
Start with three families of metric, because each one catches a different class of regression. Transfer size catches the marketing team adding a hero video. Request count catches a dependency tree quietly growing. Timing catches everything else, but it is the noisiest, so treat it as a backstop rather than your primary signal.
For transfer size, the useful unit is compressed bytes over the wire, not the size of the file on disk. Your web server almost certainly negotiates compression, so measure what the client receives. A single curl invocation gives you both the transfer size and the request count for one document:
curl -s -o /dev/null -w 'size=%{size_download} time=%{time_total}\n' \
-H 'Accept-Encoding: gzip, br' https://example.test/
# size=18422 time=0.213
That tells you about the HTML document only. For a whole page you need the subresources too, and that means driving a headless browser or parsing a HAR file. The principle stays the same: sum the compressed response bodies, count the entries, and compare against the budget. If you have a HAR file from any browser devtools session, a small script over the JSON will do it, and you can keep that script in the repository next to the config it checks.
Set the numbers from your own baseline, not from someone else's
There is no universal correct budget. A text heavy documentation site and a media heavy product page have nothing useful to say to each other. What matters is that your limits are derived from a measurement you took yourself, on a real page, on a realistic connection, and then tightened deliberately. Take the current number, decide how much headroom you are willing to give the next change, and write that down. If a page currently ships a certain number of kilobytes, a budget slightly below that forces every future change to justify itself.
Do the same for timing, but use a percentile rather than a single run. One measurement is noise. Run the check several times and look at the median or the seventy fifth percentile. A budget expressed as a single sample will flap and you will start ignoring it, which is worse than not having one.
Enforce the budget in the response headers
The cheapest enforcement point is the server itself. If you can put a hard ceiling on what a route is allowed to emit, you catch the regression at the moment it is introduced rather than in a dashboard nobody reads. Most application servers let you cap response body size, and reverse proxies let you cap request and response buffers.
Where the platform supports it, emit a custom header describing the budget so that monitoring and CI can read the same source of truth the application uses:
# nginx: advertise and cap the document budget for this location
location / {
add_header X-Perf-Budget-Bytes 250000 always;
add_header X-Perf-Budget-Requests 40 always;
client_max_body_size 1m;
}
Custom headers cost a few bytes and are trivially greppable. A CI job can fetch the page, read X-Perf-Budget-Bytes, compare it with the measured transfer size, and exit non zero on a breach. That is the whole mechanism. The header is documentation that cannot drift, because it is generated by the same config that serves the traffic.
Be careful about what you cap and where. A limit on the HTML document is easy to reason about. A limit on total page weight has to account for third party resources you do not control, and those can change without any commit in your repository. If a third party script is in your budget, pin it and monitor it, or exclude it and track it separately. Do not pretend you control something you do not.
Wire the check into CI and into review
The build is where a budget earns its keep. Add a step that measures a representative page and fails the build when a limit is exceeded. Keep the failure message specific, because a vague failure gets bypassed. The person reading the log should see which metric broke, by how much, and which asset is responsible.
A useful shape for that check is a script that returns a non zero exit code and prints the offending resources. You can build it on top of the same curl and HAR parsing you already use locally, so there is one implementation rather than two. Store the budget values in a single file, for example perf-budget.json, and have both the server config generator and the CI job read from it. Two sources of truth is how budgets die.
Then make the numbers visible in review. When a pull request adds a dependency or an image, the diff should show the budget impact. Some teams post that as a comment on the pull request, which is enough. The point is not the tooling. The point is that the cost of a change becomes visible at the moment someone can still cheaply reverse it.
Handle legitimate breaches deliberately
Budgets will be exceeded for good reasons. A new feature genuinely needs more bytes. The correct response is to change the budget in the same commit that changes the page, with the reason in the commit message, so the history records the decision. What you must avoid is the silent override, a flag that skips the check, or a threshold quietly raised in a separate commit. Those turn the budget into decoration.
It also helps to have a second, looser budget for the worst case route. Not every page needs to be as tight as the landing page, and pretending otherwise pushes people to game the check rather than fix the page. Define a small number of tiers, apply the right tier per route, and keep the tiering itself under review.
What to do next
Measure one real page today with curl and a HAR capture, write the three numbers down, and commit them as a JSON file. Add a CI step that fails on a breach, and add the matching headers to your server config so the budget is visible at runtime. Do that for a single page before you try to cover the whole site. Once the loop works end to end, extending it to more routes is mechanical, and the budget stops being a document and starts being a constraint the site grows inside.
