Performance work goes wrong in a predictable way: someone runs Lighthouse, sees an orange score, and starts optimising whatever the report listed first. Two days later the score has moved four points and nobody can tell why.
The fix is to measure in a fixed order, from the outside in, and to only change one thing at a time. This checklist follows that order.
Before you measure anything
Three setup decisions determine whether your numbers mean anything.
- 1
Test the page users actually land on
Not the homepage, unless that's where traffic goes. Check analytics for your top entry pages and test those. A product page with a gallery and reviews behaves nothing like a marketing homepage.
- 2
Decide between lab and field data
Lab data (Lighthouse, WebPageTest) is reproducible and lets you compare before and after. Field data (the Chrome UX Report, or your own RUM) is what Google uses for ranking and what your users actually experience. You need both, and they will disagree.
- 3
Throttle honestly
Testing on a wired connection with a fast laptop tells you nothing about a mid-range Android on 4G. Use DevTools' Network throttling at Slow 4G and CPU throttling at 4× as a baseline, and don't be alarmed the first time.
1. Core Web Vitals: the three numbers that matter
| Metric | Good | What it measures |
|---|---|---|
| LCP (Largest Contentful Paint) | ≤ 2.5s | When the main content finished rendering |
| INP (Interaction to Next Paint) | ≤ 200ms | How quickly the page responds to input |
| CLS (Cumulative Layout Shift) | ≤ 0.1 | How much things move around while loading |
INP replaced First Input Delay in 2024, and it's a much harsher measure: it looks at every interaction across the visit, not just the first one. Sites that scored well on FID often score poorly on INP.
Diagnosing LCP
First find out what the LCP element is, because people are wrong about this constantly. Chrome marks it in the Performance panel timeline. It's usually a hero image or a large heading.
Then break the time down. LCP has four parts: time to first byte, resource load delay, resource load time, and render delay. Which part dominates tells you what to fix:
- Slow TTFB — server, database or CDN problem. No amount of front-end work fixes it.
- Long load delay — the browser found out about the image late. It's being discovered by JavaScript, or it's behind a lazy-load that shouldn't apply. Preload it.
- Long load time — the image is too big. See the image analysis guide.
- Long render delay — render-blocking CSS or JS is holding the paint back.
Diagnosing CLS
Layout shift comes from a short list of causes, and they're all preventable:
- 1Images and videos without
widthandheightattributes (or anaspect-ratio) - 2Ads, embeds and iframes injected into unreserved space
- 3Web fonts swapping in with different metrics
- 4Content inserted above existing content, like a cookie banner that pushes the page down
- 5Animating
top/left/heightinstead oftransform
Chrome's Performance panel flags shifts in the Experience track, and clicking one shows the element that moved. For a whole-page sweep instead, Missing Width/Height Attributes in WDT finds every image that hasn't reserved its space. It's the cheapest CLS fix there is.
Diagnosing INP
INP is a main-thread problem. Long tasks block input handling, and long tasks come from large JavaScript bundles, expensive event handlers, and layout thrashing in loops. Record an interaction in the Performance panel and look for tasks over 50ms in the main thread flame chart.
2. Payload: how much are you actually sending?
Before optimising anything clever, count the bytes. Open the Network panel, disable cache, hard-reload, and read the summary bar at the bottom: number of requests, transferred size, resource size, load time.
Rough targets for a content page, uncompressed sizes:
- HTML — under 100KB
- CSS — under 100KB total, and one or two files, not fifteen
- JavaScript — under 300KB is a reasonable ceiling; under 150KB is good
- Images — under 1MB total for the initial viewport
- Fonts — two families maximum,
woff2only, subset if you can - Total requests — under 50 for the initial load
These aren't laws. They're the point at which you should have a reason. Web Developer Tools reports the same totals in one click through Resource Count Summary and Resource Type Breakdown, and Find Large Resources surfaces anything over 1MB immediately.
3. Render-blocking resources
Anything in <head> that blocks parsing delays your first paint. Synchronous scripts block; stylesheets block rendering; @import inside CSS blocks and serialises the fetch.
<!-- Blocks parsing until fetched and executed -->
<script src="/analytics.js"></script>
<!-- Downloads in parallel, runs after parse -->
<script src="/analytics.js" defer></script>
<!-- Downloads in parallel, runs whenever ready — order not guaranteed -->
<script src="/analytics.js" async></script>defer is the right default for almost everything. async is for independent scripts that don't touch the DOM. Neither is right for a script that must run before first paint, of which there are far fewer than most codebases assume.
Two tools name the offenders directly: Detect Render Blocking CSS and Detect Render Blocking JS. Quicker than reading a Lighthouse opportunity list and mapping each entry back to a tag by hand.
4. Third-party scripts
This is usually where the real weight is, and it's usually the part nobody owns. Analytics, tag managers, chat widgets, A/B testing, heatmaps, consent managers, ad tech. Each one was added for a good reason by someone who has since left.
Audit them properly:
- 1
List every third-party domain the page contacts
WDT's Detect Third-Party Scripts, Third-party Request Report and Domain Breakdown produce this list. Tag managers make it longer than you expect, because they load things that load things.
- 2
Measure the cost of each
In DevTools, block a domain (right-click a request → Block request domain) and reload. The difference in LCP and total blocking time is that vendor's real cost.
- 3
Ask whether anyone still uses it
The heatmap tool nobody has logged into for a year is pure cost. This is a conversation, not a code change, and it's often the highest-leverage one available.
- 4
Defer what survives
Almost nothing third-party needs to run before first paint. Load it after the page is interactive, or on interaction.
5. Caching and delivery
Repeat visits should be dramatically faster than first visits. If they're not, your cache headers are wrong. Check that hashed assets get a long max-age with immutable, that HTML has a sensible short policy, and that text responses come back with Content-Encoding: br or gzip.
The HTTP headers guide covers exactly what to look for. WDT's Detect Cache Headers, Detect Compression and Cache Hit / Miss Summary answer these without opening the Network panel.
6. DOM size and rendering cost
A very large DOM makes every style recalculation and layout more expensive, and it's a common cause of poor INP on pages that look simple. Lighthouse warns above roughly 1,400 nodes; the practical pain starts higher, but depth matters as much as count.
Run the DOM Size Analyzer and you get total nodes, maximum depth and maximum children in one place. Deeply nested wrappers from a component library are the usual cause, and the fix is structural rather than a tweak.

7. Fonts
woff2only — every browser you care about supports itfont-display: swapso text is visible immediately, oroptionalif you'd rather avoid the swap entirely- Preload the one or two faces used above the fold, and no more
- Subset to the characters you need if you're shipping a large family
- Match fallback metrics with
size-adjustandascent-overrideto reduce the shift when the real font arrives
The Font Usage Report lists every face the page actually loaded. It's common to find four weights loaded and two used.
The order to work in
If you do nothing else, do these five in this sequence. They're ordered by effort-to-impact ratio, which is not the order any automated report will give you.
- 1Fix the LCP image: correct format, correct size, preloaded, not lazy-loaded
- 2Add
width/heightto every image to kill the easy CLS - 3Move every non-essential script to
deferand audit the third parties - 4Turn on compression and fix cache headers
- 5Then, and only then, start splitting bundles
Bundle splitting is the work people want to do first because it feels like real engineering. It's usually fourth or fifth in terms of what actually moves the numbers.
Images are almost always the biggest single win, so auditing them properly is the natural next step. Before a release, fold the performance pass into the wider pre-launch QA checklist.