A Core Web Vitals budget you can actually keep
LCP, CLS and INP are not abstract scores — each one maps to a specific resource decision. Give every decision a number and the score takes care of itself.
Lighthouse scores are a symptom. The causes live in four places: bytes, blocking requests, layout shifts and main-thread work. Budget each one, and the score follows without you ever opening the audit tab.
The four numbers
| Metric | Target | The decision it forces |
|---|---|---|
| TTFB | < 100 ms at origin | cache the rendered page, never query the DB on a repeat view |
| LCP | < 1.0 s on Fast 3G | text LCP, no hero image above the fold, no webfont |
| CLS | 0.00 | every image has intrinsic dimensions, nothing injects above content |
| INP | < 50 ms | under 3 KB of JS, no long tasks during interaction |
The first row is the one most CMSs lose. A WordPress page with six plugins runs dozens of queries before the first byte of HTML exists. Caching plugins paper over it, but the miss path still costs 400–900 ms.
Render once, then remember it
If the document is a pure function of the database, you only need to compute it when the database changes. That is the whole caching design here:
- Render the page, gzip it, store it in memory.
- Serve repeat requests from memory — no query, no template execution.
- Bump a generation counter on any write; every cached entry becomes unreachable at once.
The last step matters more than it looks. Per-post invalidation is where caching systems rot: eventually someone forgets to invalidate a related-posts block and you ship stale content. A generation counter cannot be subtly wrong.
Layout stability is a markup problem
CLS comes from not knowing an element’s size until it loads. Fix it in markup: width and height on every <img>, aspect-ratio on media containers, no banners that push the article down, and no webfont swap that changes line heights.
This site has exactly one shift source left: images a reader uploads without dimensions. Those are measured at upload time and written into the tag.
INP is usually someone else’s code
Third-party tags are the most common cause of a bad INP, and they are almost never measured against a budget. The default here is zero external scripts. If you add analytics, add it with a number attached and check the number afterwards.
What “verified” means
Numbers on a blog post are marketing until you can reproduce them. The repo ships two scripts: one measures page weight from the rendered response, one measures TTFB across a few hundred requests and prints p50/p95/p99. Run them on your own machine before you believe anything above.