My LCP Was 7.0 Seconds. My Structured Data Was Perfect.

BackerLeader ●1 ●2 ●11
calendar_today ago • schedule3 min read

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.

🔥 Join developers growing publicly
Share your knowledge, build in public, and grow your developer presence with a global community.

More Posts

How I Built a React Portfolio in 7 Days That Landed ₹1.2L in Freelance Work

Dharanidharan - Feb 9

The Memory Leak in addEventListener You’re Ignoring

Joemetry - Oct 15

Core Web Vitals Benchmarks for Shopify Stores (2026 Data)

ApogeeWatcherverified - Sep 16

TypeScript Complexity Has Finally Reached the Point of Total Absurdity

Karol Modelski - Apr 23

I’m a Senior Dev and I’ve Forgotten How to Think Without a Prompt

Karol Modelski - Mar 19
chevron_left
1.8k Points • 14 Badges
Rochelle Park, New Jersey, US • bryanhamilton.info
5Posts
2Comments
3Connections
Full-stack web engineer · PHP & vanilla JS · 25+ years · Founder of The DJ Calendar · Open to work

Related Jobs

View all jobs →

Commenters (This Week)

1 comment
1 comment
1 comment

Contribute meaningful comments to climb the leaderboard and earn badges!