Core Web Vitals Debugging Checklist for QA Engineers
"The page feels slow" is not a bug report. This checklist turns it into one: which metric is failing, how to reproduce it, where the time actually goes, and what to hand the developer so the fix doesn't bounce back.
By Sharaj Rewoo · Updated 25 September 2026
The three metrics and their thresholds
Core Web Vitals are three user-centric measurements Google uses to describe loading, visual stability and responsiveness. Each has a "good" threshold, and a page passes when it meets that threshold for at least 75% of real page views (the 75th percentile, or p75), measured separately on mobile and desktop.
| Metric | What it measures | Good | Needs improvement | Poor |
|---|---|---|---|---|
| LCP — Largest Contentful Paint | When the largest image or text block in the viewport finished rendering | ≤ 2.5 s | 2.5–4.0 s | > 4.0 s |
| CLS — Cumulative Layout Shift | How much visible content unexpectedly moves (largest burst of shifts) | ≤ 0.1 | 0.1–0.25 | > 0.25 |
| INP — Interaction to Next Paint | Delay between a click, tap or key press and the next frame being painted, across the whole visit | ≤ 200 ms | 200–500 ms | > 500 ms |
INP replaced First Input Delay (FID) as a Core Web Vital in March 2024. The difference matters for testing: FID only measured the input delay of the first interaction, so a page could pass FID while every later click stalled. INP looks at essentially every interaction in the visit and reports one of the worst, and it includes processing time and rendering time, not just the wait before your handler starts. If an old test plan still says "check FID", update it.
Lab data vs field data — know which one you're looking at
Most perf arguments between QA and development are really arguments between two data sources.
- Lab data — Lighthouse, PageSpeed Insights' lab section, the DevTools Performance panel. A single controlled load on simulated hardware and network. Repeatable, great for diagnosis and before/after comparisons. It can't measure INP properly because nobody is clicking, so Lighthouse reports Total Blocking Time (TBT) as a proxy.
- Field data — the Chrome User Experience Report (CrUX), your own RUM (real-user monitoring), or a real-session observer running in your browser. What actual users experienced with real caches, real devices, real API latency and real interaction. This is what Google uses for Core Web Vitals assessment.
A green Lighthouse score does not mean the page passes Core Web Vitals, and a red one doesn't mean it fails. Lab runs a cold load on an empty session; users arrive logged in, with a cart, a feature flag and a third-party chat widget. Use field data to decide whether there's a problem and which metric; use lab tools to find out why.
A useful middle ground for QA is measuring your own real sessions while you test. PerfLens does this: it observes the pages you actually browse and reports live LCP, INP and CLS, long tasks, layout shifts and resource weight, entirely on-device, with a DevTools panel for the detail. That lets you attach "measured during this exact flow" numbers to a bug instead of a synthetic score.
Before you start: set up a reproducible environment
- Use a clean profile or Incognito so your own extensions don't inject scripts and skew timings (disable everything except the tools you're measuring with).
- Throttle deliberately. In the DevTools Performance panel, set CPU throttling (4× or 6× slowdown approximates a mid-range phone on a fast laptop) and a network preset such as "Fast 4G" or "Slow 4G". Write down what you used — a perf bug without its throttling settings can't be reproduced.
- Decide cold vs warm. Test a first visit (Application → Clear site data, or "Disable cache" in the Network panel) and a repeat visit. Some regressions only appear on one of them.
- Test the real state. Logged in, realistic data volume, the same locale and viewport as the affected users. A table with 10 rows and one with 2,000 rows are different pages.
- Run it more than once. Take at least three recordings and report the median. Single runs lie.
LCP checklist
LCP breaks down into four sub-parts. Finding which one is big tells you who owns the fix.
- Time to First Byte (TTFB) — server response time. Slow here means backend, CDN or redirects.
- Resource load delay — the gap between TTFB and the browser starting to download the LCP resource. Big here means the browser discovered the image late.
- Resource load duration — how long the image or font itself took to download. Big here means file size or bandwidth.
- Element render delay — time from resource loaded to actually painted. Big here means render-blocking CSS/JS or main-thread work.
How to reproduce: record a page load in the Performance panel (the reload button in the panel). The LCP marker appears on the timings track; click it to highlight the element. Newer versions of Chrome also show an LCP breakdown in the insights sidebar.
What to check:
- What is the LCP element? Often it's not what you expect — a hero image on desktop, a paragraph of text on mobile, a cookie banner on first visit.
- Is the LCP image lazy-loaded?
loading="lazy"on an above-the-fold image is one of the most common LCP regressions. - Is the image discoverable in the initial HTML, or injected by JavaScript or set as a CSS background? Late discovery shows up as load delay.
- Does it have
fetchpriority="high"or a<link rel="preload">? Check the Network panel's Priority column. - Is it oversized? A 2400px image rendered at 400px wide, or a PNG where AVIF/WebP would do.
- Is it served from a different origin that needs a fresh DNS + TLS handshake?
- For client-rendered apps: is the content waiting on an API call before anything renders? Look for a long gap with the main thread idle and a fetch in flight.
- For text LCP: are web fonts blocking text rendering? Check
font-display.
CLS checklist
CLS is the one QA catches best, because it's the one users feel as "I clicked the wrong button". It's measured over the whole page lifetime, not just load, and it's reported as the largest burst ("session window") of shifts.
How to reproduce: in the Performance panel, layout shifts appear on their own track; select one to see which elements moved. In the Rendering drawer (⋮ → More tools → Rendering), enable "Layout Shift Regions" to flash shifted areas in blue as they happen. Shifts within 500 ms of a user input are excluded from CLS by design, so a menu opening after a click is fine; content jumping on its own is not.
What to check:
- Images, videos and iframes without
width/heightattributes or a CSSaspect-ratio. - Ads, embeds and consent banners inserted above existing content without reserved space.
- Web fonts swapping in with different metrics, making text reflow (look for a shift right after the font request finishes).
- Skeleton screens whose height doesn't match the real content.
- Content injected after an API response: "You have 3 new messages" banners, personalised recommendations, A/B test variants.
- Animations using
top/left/heightinstead oftransform. - Shifts after load: infinite scroll, lazy components, sticky headers changing height on scroll.
Test at multiple viewport widths. A layout that's stable at 1440px can shift badly at 375px when a line of text wraps.
INP checklist
INP is the hardest to test because it depends on what the user does. Lab tools that only load the page will never find it — you have to interact.
Each interaction has three phases:
- Input delay — the main thread was busy with something else when you clicked.
- Processing time — your event handlers running.
- Presentation delay — style recalculation, layout and paint needed to show the result.
How to reproduce: start a Performance recording with CPU throttling on, perform the slow interaction, stop. The Interactions track shows each interaction with its duration; click one to see the phases and the long task underneath it. The live metrics view in the Performance panel also shows INP updating as you interact, with the slowest interaction called out.
What to test and check:
- Interact during page load, not just after. Clicks while hydration or third-party scripts are running are where input delay lives.
- Typing in search boxes and filters — a handler that re-filters a large list on every keystroke is a classic INP failure.
- Opening menus, modals, accordions and tabs on data-heavy pages.
- Actions that update large DOM trees: sorting a table, toggling "select all", switching views.
- Handlers doing synchronous work before giving visual feedback: analytics calls, JSON parsing, heavy state updates.
- Layout thrashing: code reading layout (
offsetHeight,getBoundingClientRect) and writing styles in a loop, visible as repeated purple "Layout" blocks. - Very large DOM size, which makes every style recalculation and layout more expensive.
The typical fixes are to show feedback first and do the heavy work after (yield to the main thread, e.g. with setTimeout or scheduler.yield() where supported), debounce input handlers, virtualise long lists, and move expensive computation off the main thread.
Long tasks: the common root cause
A long task is any piece of main-thread work that runs for more than 50 ms. While it runs, the browser can't respond to input or paint — which is why long tasks drive both INP (input delay) and LCP (render delay) problems, and why Lighthouse's TBT correlates with INP.
In the Performance panel, long tasks are marked with a red triangle on the main thread track. Expand one and read the flame chart bottom-up: the widest bars at the bottom are where the time is. The Bottom-Up tab, grouped by domain or by URL, quickly shows whether the time belongs to your bundle or to a third party. That distinction decides who gets the bug — and it's worth writing in the report.
Common offenders: large JavaScript bundles evaluated on load, framework hydration of the whole page, tag managers and analytics, A/B testing scripts that run synchronously in the head, chat widgets, and JSON parsing of large API responses.
How to write a performance bug report that gets fixed
Perf bugs get closed as "can't reproduce" more than any other kind. A good report removes every reason to do that:
- Metric and value — "INP 640 ms when clicking 'Apply filters'", not "filters are slow". Include the threshold it fails.
- Where it came from — field (CrUX/RUM, and at what percentile) or lab. If lab, the median of how many runs.
- Exact steps — URL, user state, data volume, and the interaction. A recorded repro removes ambiguity; Gotcha captures repro steps, console errors, failed network requests and the environment in one click if you want the whole bundle filed to Linear or Jira.
- Environment — browser version, device or CPU/network throttling, viewport, cold or warm cache, logged in or not.
- Evidence — the exported Performance trace (Save profile), plus an annotated screenshot of the offending long task or layout shift. For CLS, a before/after capture of the element that moved is worth a paragraph; Hotshot can snap to an element and annotate it.
- Your diagnosis — which LCP sub-part or INP phase dominates, and the script or element responsible. You don't have to write the fix, but "90% of the delay is a 480 ms task in
vendor-chat.js" routes the bug to the right person immediately. - Baseline — if it's a regression, the numbers from the last good build. Regressions with a baseline get fixed; vague slowness gets backlogged.
Make it part of the regression cycle
Performance regresses quietly, one dependency or one tracking script at a time. A few habits keep it visible:
- Keep a short list of key pages and key interactions (search, add to cart, open record, save form) and measure them every release with the same throttling settings.
- Track numbers over time, not pass/fail — a slide from 1.8 s to 2.4 s LCP still "passes" but is a regression worth flagging.
- Add a performance budget check in CI (Lighthouse CI or similar) for the lab-measurable parts, and use field data to confirm what users see after release.
- Re-test after adding any third-party script. It's the single most common cause of sudden regressions.
Quick reference
| Symptom | Likely metric | First place to look |
|---|---|---|
| Hero appears late | LCP | Lazy-loaded or late-discovered LCP image, TTFB |
| Blank page, then everything at once | LCP | Render-blocking JS/CSS, client-side rendering waiting on API |
| Clicked the wrong thing | CLS | Unsized images, injected banners, font swap |
| Button does nothing for a moment | INP (input delay) | Long tasks during load, third-party scripts |
| Typing lags | INP (processing) | Per-keystroke handlers, no debounce |
| UI freezes after click on big pages | INP (presentation) | Large DOM updates, layout thrashing |