A practical Core Web Vitals checklist for small product teams
Technology · 2026-07-12 · 8 min read · 1750 words
By ESPYCRUX, Studio
Most Core Web Vitals advice is written for teams with a performance engineer. This is the checklist we actually work through on a small studio's sites — what the three metrics mean, the specific fixes that move each one, and the two mistakes that make good numbers lie.
Core Web Vitals is a short list, which is what makes it useful. Three metrics, three thresholds, one percentile. You can hold the whole thing in your head, which is more than can be said for most performance advice.
The trap is that the fixes are not short, and most guides list every possible one. On a small team you need the handful that actually move the number, in the order worth doing them.
Here is what we work through.
The three metrics, and what they actually measure
| Metric | Measures | Good | Needs work | Poor |
|---|---|---|---|---|
| LCP — Largest Contentful Paint | How quickly the main content appears | ≤ 2.5s | 2.5–4.0s | > 4.0s |
| INP — Interaction to Next Paint | How quickly the page responds when touched | ≤ 200ms | 200–500ms | > 500ms |
| CLS — Cumulative Layout Shift | How much the layout moves unexpectedly | ≤ 0.1 | 0.1–0.25 | > 0.25 |
Two things about how these are judged, both of which people get wrong:
You are scored at the 75th percentile, not the average. If three quarters of your visits are fast and a quarter are slow, you are measured by the slow quarter's boundary. An average hides exactly the visits that fail you.
Mobile and desktop are segmented separately. Passing on desktop tells you very little. The score that usually matters is mobile, on a mid-range phone, on a real network.
INP replaced First Input Delay as the responsiveness metric in 2024. If you are working from an older guide that talks about FID, it is out of date — and the difference is meaningful. FID measured only the delay before the first interaction was processed. INP measures the full latency of interactions across the whole visit, including the paint. It is a much harder metric to pass with a heavy JavaScript app, and that is the point.
LCP — get the main thing on screen
LCP is almost always one of: a hero image, a heading, or a large block of text. Find out which before optimising anything — Chrome DevTools' Performance panel labels the LCP element directly.
1. Stop it being discovered late. The single most common LCP failure is an image the browser cannot find until it has parsed and executed something else. If your hero image is set by JavaScript, or lives inside a component that hydrates, the browser learns about it far too late.
Put it in the initial HTML and mark it:
<img src="/hero.avif" alt="…" fetchpriority="high" decoding="async"
width="1344" height="768">fetchpriority="high" tells the browser this is the one that matters. Everything below the fold should be loading="lazy" — and your LCP image should never be, which is a surprisingly common mistake when a team adds lazy loading globally.
2. Preconnect to whatever serves it. If fonts or images come from another origin, the browser must do DNS, TCP and TLS before it can even ask:
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>Two or three of these help. A dozen hurt — each one costs a connection.
3. Serve a modern format at the right size. AVIF or WebP, and not a 2400px image scaled down in CSS to 600px. This is dull and it is usually the biggest single win.
4. Do not let a font block the text. If your LCP element is text, a web font with default loading behaviour will hide it while the font downloads. font-display: swap renders immediately in a fallback and swaps when ready. If you use Google Fonts, add &display=swap to the URL — which their generated tag does not always include.
5. Watch what your framework does before first paint. A client-rendered app cannot paint meaningful content until its bundle has downloaded, parsed and executed. If LCP is your worst metric and you are shipping a single-page app, this is usually why, and no amount of image tuning will fix it. Prerendering or server rendering is the actual answer.
INP — the one that catches modern apps
INP is where component-heavy applications get punished, because it measures the thing those apps are worst at: doing a lot of work in response to a tap.
1. Find long tasks. Anything blocking the main thread for more than 50ms delays every interaction that arrives during it. In DevTools' Performance panel they are the blocks flagged with a red corner. You are looking for the ones that run in response to input.
2. Stop re-rendering on high-frequency events. This is the most common self-inflicted INP problem we see, and we had it ourselves. A card that tilts toward the cursor was calling setState on every mousemove — re-rendering the card and its children dozens of times a second and allocating a new state object each time.
The fix is not to throttle it. It is to stop involving the framework at all:
function onPointerMove(e) {
const el = e.currentTarget;
const r = el._rect || (el._rect = el.getBoundingClientRect());
el.style.setProperty('--rx', `${((e.clientY - r.top) / r.height - .5) * -9}deg`);
el.style.setProperty('--ry', `${((e.clientX - r.left) / r.width - .5) * 9}deg`);
}Two custom properties written straight onto the node. No state, no re-render, and the rect is cached on pointerenter rather than read on every event — because getBoundingClientRect() in a move handler forces layout on every single one.
3. Get scroll work off the main thread entirely. A scroll listener that toggles a class re-renders your tree on every scroll event. Most of what those listeners do can now be expressed as a CSS scroll-driven animation, which runs on the compositor and executes no JavaScript. Progress bars, parallax and reveals are all better done that way.
4. Yield before doing heavy work. If a click genuinely must do something expensive, let the browser paint the response first:
button.addEventListener('click', async () => {
showPending(); // immediate visual feedback
await new Promise(r => setTimeout(r, 0)); // yield so it can paint
doExpensiveThing();
});INP measures interaction to next paint. Painting something quickly, then working, scores far better than working and then painting — and it feels better too.
5. Audit third-party scripts. Analytics, chat widgets, ad tags and A/B testing tools all run on your main thread. A single badly behaved tag can dominate your INP, and you will not find it by reading your own code. Load what you can with async or defer, and be ruthless about what earns its place.
CLS — reserve the space
CLS is the easiest to fix and the easiest to leave broken, because it does not show up when you develop on a fast connection with everything cached.
1. Dimensions on every image and video. Always:
<img src="/thing.avif" alt="…" width="800" height="600">The attributes give the browser an aspect ratio to reserve before the file arrives. CSS can still size it responsively — width: 100%; height: auto — but the ratio prevents the reflow.
2. Reserve space for anything that arrives late. Embeds, ad slots, cookie banners, anything loaded conditionally. If a container will be 250px tall once filled, make it 250px tall now. An element that appears and pushes the article down is the single most irritating layout shift there is.
A related point, learned the hard way: do not ship dummy placeholder boxes as content. Reserving space is a layout technique, not a design element — a page covered in dashed boxes reading "Advertisement" reads as a template built to host ads, whatever it does for CLS.
3. Match your font metrics. A fallback font with different metrics causes a visible jump when the web font swaps. size-adjust and ascent-override on an @font-face let you tune the fallback so the swap is close to invisible.
4. Never insert above existing content. A banner injected at the top of the page pushes everything down and is guaranteed CLS. If you must have one, reserve its space in the initial layout or overlay it.
The two mistakes that make good numbers lie
Testing on your own machine. A developer laptop on office broadband with a warm cache is not your 75th percentile visitor. Test throttled — DevTools' "Slow 4G" and 4× CPU slowdown is a reasonable proxy for a mid-range Android phone. Numbers will get much worse, and those are the real numbers.
Trusting lab data alone. Lighthouse is a lab tool: one synthetic load, in a controlled environment. It cannot measure INP properly, because INP depends on real people interacting in ways a synthetic run does not reproduce.
For field data, use the Chrome UX Report — that is the same source Google uses. If your site does not get enough traffic to appear in CrUX, collect it yourself with the web-vitals library and send it somewhere you can read it. A small site with no field data is flying blind, and lab scores will flatter you.
The checklist
Work down it. Most sites fail on three or four of these, not twenty.
LCP
- I know which element is my LCP element
- It is in the initial HTML, not injected by script
- It has
fetchpriority="high"and is not lazy-loaded - Images are AVIF or WebP, sized to their display size
- Fonts use
font-display: swap preconnectto critical third-party origins, and no more than a few- Meaningful content does not wait on a JavaScript bundle
INP
- No
setStateinmousemove,scrollorresizehandlers - No
getBoundingClientRect()inside a high-frequency handler - Scroll effects are CSS, not listeners
- Expensive click work yields before running
- Third-party scripts audited,
async/deferwhere possible - Long tasks checked in the Performance panel, on a throttled CPU
CLS
- Every image and video has
widthandheight - Late-arriving content has reserved space
- Nothing is inserted above existing content
- Font fallback metrics adjusted
Measurement
- Tested throttled, not on a developer machine
- Mobile and desktop checked separately
- Field data collected, not just Lighthouse
- Judged at the 75th percentile
What we would do first
If you have limited time, in this order:
- Add
widthandheightto every image. Fastest CLS fix in existence, usually an afternoon. - Fix your LCP image's priority. One attribute, frequently worth a second or more.
- Find the one handler doing too much. There is almost always exactly one — a mousemove, a scroll listener, a search input filtering on every keystroke.
Then measure again, throttled, and only then start on the long tail. Performance work has sharply diminishing returns, and a small team's time is better spent shipping the next thing than chasing a score from 92 to 96.
Thresholds and percentile per web.dev — Web Vitals. INP replaced FID as a Core Web Vital in 2024.
Tags: performance, core web vitals, inp
ESPYCRUX — ESPYCRUX is a small product studio based in India, building focused web applications and writing about the engineering behind them. Articles are written by whoever did the work, and published under the studio name. Reach the studio at admin@espycrux.com.