Why "keep it fast" fails
Everyone on a WordPress project agrees the site should be fast. Then a plugin is added for a form, a slider for the home page, a fonts pack for the rebrand, a chat widget for sales, and each was reasonable and each was a little slower, and a year later the site fails Core Web Vitals and nobody made a decision to make it slow.
A performance budget is the mechanism that turns the agreement into a constraint. It is a small set of numbers the site must stay under, measured on every change, with a rule about what happens when it does not. It is how a site stays fast rather than starts fast.
What to budget
Pick metrics that are cheap to measure, hard to game, and predictive of the user experience. For a business WordPress site, five:
Total page weight (compressed transfer size) for the home page and one template of each type. The single best predictor of load time on slow connections.
Number of requests. Each is a round trip. Also a good proxy for "how many plugins load assets on this page".
JavaScript weight, separately, because it costs CPU as well as bandwidth and is the main driver of interaction delay.
Largest Contentful Paint, lab, on a throttled mobile profile. The user-facing metric Google grades.
Total Blocking Time (lab) as a stand-in for INP, since INP cannot be measured without interaction.
Optionally: CLS (should be near zero and stay there), and number of third-party origins.
Sensible limits for a business site
These are the budgets we set for a typical block-theme business site. Adjust for content-heavy pages, but not by much.
| Metric | Home | Inner page | |---|---|---| | Page weight (compressed) | 800 KB | 600 KB | | Requests | 35 | 30 | | JavaScript (compressed) | 150 KB | 120 KB | | LCP (lab, mobile throttled) | 2.0 s | 1.8 s | | TBT (lab, mobile throttled) | 150 ms | 100 ms | | CLS | 0.05 | 0.05 | | Third-party origins | 4 | 3 |
A well-built block theme with optimised images comes in under all of these with room. A page-builder site with a slider and a chat widget does not, which is the point: the budget makes the cost of those visible before they ship.
Set the budget with headroom below Google's thresholds (LCP 2.5s, INP 200ms), because field performance on real phones is worse than lab.
Measuring on every change
The budget only works if it is measured when things change, not once at launch.
Lighthouse CI is the standard tool. It runs Lighthouse against URLs on a staging deploy and compares results to a lighthouserc budget file, failing when over. It works in GitHub Actions, GitLab CI or any pipeline, and for WordPress it runs against the staging site after each deploy or on a schedule.
A budget.json for Lighthouse CI:
[
{
"path": "/*",
"resourceSizes": [
{ "resourceType": "total", "budget": 800 },
{ "resourceType": "script", "budget": 150 },
{ "resourceType": "image", "budget": 400 },
{ "resourceType": "font", "budget": 100 }
],
"resourceCounts": [
{ "resourceType": "total", "budget": 35 },
{ "resourceType": "third-party", "budget": 10 }
],
"timings": [
{ "metric": "largest-contentful-paint", "budget": 2000 },
{ "metric": "total-blocking-time", "budget": 150 },
{ "metric": "cumulative-layout-shift", "budget": 0.05 }
]
}
]For sites without a CI pipeline, a scheduled run (weekly, via a small script or a SaaS monitor) against production, with results stored and a threshold alert, catches drift within a week. Less immediate than CI, far better than nothing.
Plugin additions are the change most likely to blow the budget and least likely to go through a pipeline. The rule: any new plugin is installed on staging first, the budget is run, and the delta is written down. "Chat widget: +180 KB JS, +6 requests, +300 ms TBT" is a number the business can decide about.
What happens when a change goes over
The budget is a decision trigger, not a wall. When a change exceeds it:
- Can the change be made cheaper? Load the widget on interaction instead of on load. Use the plugin's lighter mode. Self-host the font. Dequeue the assets on pages that do not need them. Most overages are fixable this way.
- Can something else be removed to pay for it? The old slider nobody uses. The second analytics tool. Budgets are budgets; trade.
- Is the feature worth the cost? Sometimes yes: a booking widget that converts is worth its weight. Then the budget is consciously raised, with a note saying why and by whom.
- Otherwise, it does not ship.
The point of the process is that step 3 is a decision made by the business owner with the number in front of them, rather than a slow accumulation nobody chose.
Making it stick
- The budget lives in the repository next to the theme, so it is versioned and visible.
- The numbers appear in the monthly maintenance report: "Home page 640 KB of 800; JS 110 of 150; LCP 1.6 s."
- Overages are recorded with their reason, so a year later the site's weight has a history.
- The client knows the budget exists and what it is for. "We can add that; it costs about 200 KB and here is what we would remove to afford it" is a conversation clients respond well to, because it treats speed as a shared resource rather than a developer's preference.
Where this sits
A performance budget is set on every WordPress build we deliver, run in CI where the client's hosting supports a pipeline and on a weekly schedule where it does not, and reported on the care plan each month. It is the thing that lets us say a site will still be fast in year three and mean it.