Where we actually started
Our PageSpeed Insights report last year was ugly. Largest Contentful Paint sat around 4.1 seconds on mobile for our busiest tool pages, Cumulative Layout Shift spiked whenever an ad slot loaded, and the field data in Search Console showed a meaningful chunk of real users failing every Core Web Vitals threshold at once. We spent about three months methodically working through fixes, measuring each one in isolation with WebPageTest and field data, and logging what actually happened. This isn't a "10 tips" checklist — it's what moved the needle for us, what didn't, and why some of the advice you read everywhere barely mattered on a site like ours.
A caveat before the details: your mileage depends entirely on where your bottleneck actually is. We're a tools site with lots of small interactive pages, some ad inventory, and a shared layout across every route. If your app is mostly static marketing pages or a checkout flow, some of this ranking of "what mattered" will flip for you.
What actually reduced LCP
Preloading the hero image, not just compressing it
Every one of our tool pages has a hero illustration above the fold. We'd already compressed it and served WebP, so we assumed LCP was "handled." It wasn't. The browser wasn't discovering that image until it parsed well into the HTML, because it was set via a background-image in a CSS file loaded after some render-blocking scripts. Adding an explicit preload hint let the browser start the image request immediately, in parallel with everything else:
<link rel="preload" as="image" href="/images/hero-tool-illustration.webp" fetchpriority="high">
That single line, combined with switching the hero from a CSS background-image to an actual <img> tag with a high fetchpriority attribute, cut a noticeable chunk off our LCP on the pages that had it. This was the single biggest win of the whole project, and it's also the one most "top 10 web vitals" articles bury under generic "optimize your images" advice that never mentions the request wasn't starting early in the first place.
Compressing images before they ever reach production
Separately, we found a batch of tool icon PNGs being served at four or five times their display size because a designer had exported them at retina resolution for a mockup and nobody downsized them before upload. This is embarrassingly common and worth a recurring audit. We now run every image through our own image compressor before it goes anywhere near the CMS, and we check actual rendered-versus-natural dimensions in dev tools whenever a new hero graphic goes live. It's not glamorous, but oversized source images are still one of the most common LCP killers we find when we audit other people's sites too.
What actually reduced CLS
Reserving space for ad slots and late-loading embeds
This was our worst offender. Our ad slots were rendering into a container with no defined height until the ad script resolved, so the page would visibly jump the moment an ad loaded in — sometimes shoving a whole paragraph of content down the screen right as someone was about to tap something. The fix was unglamorous: give every ad container a reserved minimum height that matches the expected creative size, even when it's empty.
.ad-slot {
min-height: 250px;
contain: layout paint;
}
We did the same thing for embedded videos and social embeds — wrapping them in a container with an explicit aspect-ratio so the layout doesn't wait for the iframe to report its own height. CLS on the affected pages dropped from failing to comfortably within the "good" band, and it took less than a day to implement across the templates once we'd actually found every offending component.
font-display: swap, with a side effect worth knowing about
We switched our webfont loading to font-display: swap so text renders immediately in a fallback font instead of staying invisible while the webfont downloads:
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var.woff2') format('woff2');
font-display: swap;
font-weight: 100 900;
}
This helped perceived load time and eliminated invisible-text flashes, but it introduced its own small CLS cost: when the real font swaps in, line lengths shift slightly because the fallback font has different metrics. We reduced most of that with a font-size-adjust value tuned to match our fallback's x-height to Inter's, which isn't something most vitals guides mention because it only matters once you've already fixed the bigger, more obvious layout shift sources. It's a genuinely small effect on its own, but it was the last few points standing between us and a clean CLS pass on our worst-offending template, and those last few points are usually the most fiddly ones to track down.
What we expected to help but barely moved anything
Deferring third-party analytics scripts
We deferred and moved every non-essential third-party script to load after the main thread was idle, expecting a meaningful Total Blocking Time improvement. It helped a little on low-end devices, but nowhere near as much as the ad-slot CLS fix or the LCP preload. Our analytics payload was already fairly light, so there wasn't much blocking time left to claw back. If your site is loaded with heavier trackers, tag managers, or chat widgets, this lever will likely matter far more for you than it did for us — it's very workload-dependent, which is exactly why generic advice ranks it so inconsistently.
Switching to a CDN we'd been told would fix everything
We migrated static asset delivery to a CDN expecting a clear TTFB win. Origin latency did improve for visitors far from our server region, but for our largest user base — already geographically close to origin — the difference was within measurement noise. It wasn't wasted effort, since it removed a single point of failure and helped our long tail of international traffic, but it wasn't the LCP fix the sales pitch implied. If your traffic is already concentrated near your server, don't expect a CDN alone to rescue a bad LCP score.
The one INP fix nobody talks about enough
Interaction to Next Paint replaced First Input Delay in the official metric set, and the fix that helped us most wasn't "reduce JavaScript" in the abstract — it was finding one specific event handler on our tool result pages doing synchronous DOM measurement (reading offsetHeight in a loop) on every keystroke. Pulling that measurement out of the hot path and batching it with requestAnimationFrame cut our worst-case interaction latency dramatically on pages with live-preview inputs. The lesson: profile actual interactions with the Chrome DevTools Performance panel rather than assuming "less JS overall" is the fix. Sometimes it's one bad function, not your bundle size.
A short checklist worth running before you touch anything else
- Open your LCP element in DevTools and confirm the browser is discovering its resource early — not just that the file itself is small.
- Give every dynamically-injected element (ads, embeds, cookie banners) a reserved size before it loads.
- Audit real rendered image dimensions versus natural file dimensions on your top-traffic pages.
- Profile actual click and keypress handlers on interactive pages instead of guessing where INP problems live.
- Re-measure with field data, not just a single lab run, before declaring victory on any of this.
Was any of this related to a framework rewrite?
At one point during this project someone suggested the real fix was rewriting the whole site in a JS framework with better rendering defaults. We didn't, and it wasn't necessary — most of what moved our numbers was template-level and asset-level, not framework-level. If you're weighing that kind of bigger architectural decision for a content-heavy site, we wrote up the actual tradeoffs in our comparison of Next.js and Laravel + Blade, including where rendering architecture genuinely does matter for vitals and where it's a distraction from cheaper fixes.
Lab data lied to us once, and it's worth knowing when
Partway through this project, a Lighthouse run in DevTools showed our LCP comfortably in the "good" range on a page where Search Console's field data — real Chrome User Experience Report numbers from actual visitors — was still showing a meaningful share of users failing the same metric. The gap turned out to be device and network conditions: our lab runs were on a fast simulated connection close to our own office network, while a real slice of our mobile traffic was on genuinely slower connections and older devices than Lighthouse's default throttling profile assumes. A lab score passing doesn't mean the metric is actually fixed for your real users; it means it's fixed for the specific conditions you tested under, which may not match your traffic mix at all.
After that, we stopped treating a single green Lighthouse run as a finish line. We check PageSpeed Insights' field data section specifically, which pulls from real Chrome usage rather than a simulated run, and we treat lab tools as a fast local feedback loop for iterating on a fix, not as the final verdict on whether a fix actually worked for our actual users.
What we'd do differently next time
We'd measure LCP resource discovery timing first, before touching anything else, because it was the highest-leverage fix and we found it almost by accident partway through the project. We'd also budget more time for the CLS audit — finding every late-loading, unsized element across a large template library takes longer than it sounds, and it's tedious enough that it's tempting to skip. It's also the fix users notice most directly, because a page jumping under their thumb is the kind of thing that makes people distrust a site even when they can't articulate why.
No comments yet.
Be the first visitor to add a thoughtful comment on this article.