Originally published at bryanhamilton.info/writing/lcp-7-to-2-the-core-web-vitals-takedown/
My structured data was clean. My Rich Results warnings were zero. And then I looked at my homepage LCP and it said 7.0 seconds.
Seven seconds. For a page that is, structurally, a list of artist cards.
This is the teardown that took it to 2.3 seconds - a 67% cut - and the three traps that made a one-day fix take a week to verify.
MEASURE WHAT THE BROWSER ACTUALLY FETCHES
First rule of the whole saga: diagnose against the served page, not your source code. My stack runs LiteSpeed (page cache, \~5 min HTML) behind Cloudflare, with PHP OPcache on a 60-second revalidate. The same fix can look live, dead, and live again inside ten minutes depending on which layer you're looking at.
So every measurement followed the same ritual: cache-busted fetch of the served HTML, then read the link and script tags a real browser would see. Not the source file. The served page.
CULPRIT: RENDER-BLOCKING FONTS
The homepage's render chain started with one line in every page head:
<link href="https://fonts.googleapis.com/css2?family=Unbounded&family=Chakra+Petch&display=swap" rel="stylesheet">
One third-party stylesheet, gating first paint, on every cold visit. display=swap doesn't save the LCP element if the stylesheet itself is render-blocking - the browser still waits on the CSSOM before it knows what "swap" even means.
First pass: keep Google Fonts, stop the bleeding. Preloads for the WOFF2 files, CSS made non-blocking. That alone moved LCP meaningfully. But a third-party stylesheet dependency on a content site is a tax you pay forever - availability, latency, privacy. So the second pass removed it: self-hosted WOFF2, preloaded, font-display declared in my own CSS. The two preconnect hints for fonts.googleapis.com and fonts.gstatic.com got deleted outright - dead weight after the migration.
This ran across 129 root PHP files in one sweep. Which brings me to the gotchas.
THREE TRAPS ON THE WAY TO SHIPPED
Trap 1: the variable font parser. Unbounded ships as a variable font with weight range 400-900. A naive "grab the first number" parse produced a broken weight axis. And my first sweep aborted on the first file whose shape it didn't expect. The v3 rule: restore what you touched, log it, keep going, report at the end.
Trap 2: the helpful middleware. Cloudflare's Fonts feature was silently rewriting my Google Fonts links to its own /cf-fonts/ mirror. The origin looked fixed while the edge served old markup - and I spent a verification cycle chasing a phantom. If a CDN offers to "optimize" something for you, assume it will complicate your diff.
Trap 3: the wrong stylesheet. An earlier fix in this saga taught me this the hard way: /style.css is served via an .htaccess rewrite to a different file on disk. I patched the decoy, the change rendered nothing, and I almost shipped a second patch on top of it. Grep the rewrite rules before you patch any stylesheet, and verify the served bytes.
THE NUMBERS
Homepage LCP: 7.0s to 2.3s, measured on the production page, not a toy replica.
Still on the table: total blocking time sits around 600ms, and the Unbounded WOFF2 weighs about 50KB. The next round of this fight.
THE BORING SUMMARY
Third-party font stylesheets are render debt. Preload is a patch; self-hosting is the fix.
Cache-bust every verification. Your page cache, CDN, and OPcache are all lying to you at different intervals.
Sweeps must continue-per-file and self-report. Or they die on the first weirdo.
Verify the served page. Every time. The chain between your editor and the browser has more actors than you think.
Question for the comments: what's the longest a "simple CSS fix" has taken you to actually verify? Mine was a week, and every layer of the cache got blamed at least once.