I have a bad habit with performance work. I open Lighthouse, see a red number, and start fixing whatever the report lists first. Usually that’s an image that’s 40KB too big. It feels productive, and it moves the score, and half the time it has nothing to do with what my client’s users actually wait for.
So when Cloudflare released a dataset of real-user measurements from about 10,000 of the biggest sites on its network, I read the BEACON announcement with a specific question: if I had one afternoon to improve Core Web Vitals on a normal client site, where should it go? Short version: fix the server response and the render delay before you touch a single image, and stop testing only on Chrome.
I haven’t run BEACON’s BigQuery queries against my own client work yet, so treat what follows as a reading of Cloudflare’s published numbers plus my opinions about them, not as a lab report.
What the dataset actually is
BEACON is anonymized real-user monitoring data, refreshed daily in Google BigQuery. Cloudflare says it covers roughly 10,000 large sites and every major browser engine. Domain names and URL paths are stripped, and any group with fewer than five data points is dropped. Records are published as histograms, not averages, so you can compute your own percentiles.
That last detail matters more than it sounds. Most “average page speed” charts you see online are averages of averages. A histogram lets you ask the question Google itself asks: what happens at the 75th percentile? The official thresholds on web.dev are LCP within 2.5 seconds, INP at 200 milliseconds or less, and CLS at 0.1 or less, all measured at the 75th percentile of page loads.
One caveat before I lean on any of this. Ten thousand big sites on one CDN is not the web. A five-page brochure site on shared hosting has different problems from a site big enough to be in this sample. I’ll come back to that at the end.
Where LCP time actually goes
Largest Contentful Paint gets split into four parts: time to first byte, load delay, load duration, and render delay. Cloudflare publishes the breakdown for pages that land in the good, needs-improvement and poor buckets, and the shape is what interested me.
As I read the table, pages with good LCP have a document TTFB around 598ms, while poor ones sit near 1,891ms. Load delay goes from 76ms to about 1,485ms. Render delay goes from 157ms to about 2,002ms. Load duration, the time actually downloading the LCP resource, barely moves between buckets.
Read that again, because it is the opposite of what most tutorials teach. The advice everywhere is “compress your hero image”. But download time is the part that hardly differs between fast and slow pages. What differs is everything around the download: how late the server answers, how late the browser discovers the resource, and how long it sits idle before painting.
So here’s the order I’m going to work in from now on:
- Check TTFB first. If the HTML takes more than a second to arrive, nothing else you do will get you under 2.5 seconds on a mid-range phone.
- Check load delay. This is the gap between the HTML arriving and the browser starting to fetch the LCP image or font.
- Check render delay. Usually a blocking script or a client-side framework that hasn’t hydrated.
- Only then look at file size.
Measure the four parts on your own site
You don’t need BigQuery to find out which part hurts. The web-vitals library has an attribution build that hands you the same four numbers per page view.
import { onLCP } from 'web-vitals/attribution';
onLCP(({ value, attribution }) => {
navigator.sendBeacon('/vitals', JSON.stringify({
metric: 'LCP',
total: Math.round(value),
ttfb: Math.round(attribution.timeToFirstByte),
loadDelay: Math.round(attribution.resourceLoadDelay),
loadDuration: Math.round(attribution.resourceLoadDuration),
renderDelay: Math.round(attribution.elementRenderDelay),
element: attribution.target,
}));
});
Collect a week of that, sort by the 75th percentile of each field, and the biggest one is your afternoon. A very common cause of a fat load delay is a boring one: the hero image is set as a CSS background, so the browser can’t see it until the stylesheet has been parsed. Moving it to a real image tag with a priority hint fixes most of it.
<!-- before: browser discovers this late, inside the CSS -->
<div class="hero" style="background-image:url(/hero.webp)"></div>
<!-- after: discoverable in the HTML, fetched early -->
<img src="/hero.webp" width="1200" height="600"
fetchpriority="high" alt="Product dashboard on a laptop">
That is a two-line change and it attacks the load delay bucket, not the file size bucket. The width and height attributes also cover CLS, so you get a second win for free.
The browser engine gap nobody tests
This was the finding that made me put my coffee down. Cloudflare reports that WebKit, the engine under every iPhone browser, is the best performer overall. Fine. But in 46 countries, together over 10% of traffic, WebKit’s LCP or INP trails Blink-based browsers by at least 10%. Their example is Cambodia, where WebKit’s LCP is 50% worse than Blink’s while making up 17.5% of page views.
It’s a trap I fall into easily. Testing on desktop Chrome, maybe throttled, means looking at one engine, in one country, on good bandwidth. If your clients sell to people in Southeast Asia, Africa or South America, the browser you never open may be the one a fifth of the customers use.
The practical fix is dull. Put the field data you collect (like the beacon above) behind a breakdown by browser engine and country, and look at the tail instead of the median. If you use a hosted RUM tool, check whether it lets you segment that way. If it doesn’t, that’s a real reason to switch.
Soft navigations change what “fast” means for SPAs
The other table in the post compares hard navigations (a full page load) with soft navigations (client-side route changes in a single-page app). At the 75th percentile, hard navigations come in at 1,421ms and soft ones at 582ms. At the median it’s 791ms against 274ms. Soft navigations render two to three times faster at every percentile Cloudflare lists.
Before anyone tweets “SPAs are faster”, look at the second half of that finding: the landing page in those apps is still heavy, with a median around 1,370ms. You pay the cost once, at the door, and then every click is cheap.
That reframes a decision I get asked about a lot. If your users typically land on one page and leave, a heavy client-side framework buys you nothing and costs you the entry. If they land and then click through ten screens, the trade flips. I wrote about a related piece of this in my post on React 19.3 view transitions, where the transition itself was the thing I’d been faking with timeouts. And the entry-cost side of the story showed up in my notes on Turbopack chunking in Next.js 16.3.
Chrome measures soft navigations experimentally at the moment, so I’d treat these numbers as a preview of where measurement is going, not a metric you can put in a client report today.
INP: where the 200ms goes
For INP, Cloudflare splits the interaction into input delay, processing time and presentation delay. The good-bucket values are about 18ms, 55ms and 56ms. The poor bucket has 84ms of input delay, 284ms of processing and 217ms of presentation delay.
Processing time is where your own code lives, and it’s the bucket that grows fastest. The usual cause is one click handler doing too much before the browser gets to paint. The smallest fix I know is to yield to the main thread after the visible work is done.
button.addEventListener('click', async () => {
showSpinner(); // the thing the user must see now
// let the browser paint before the heavy part runs
if ('scheduler' in window && 'yield' in scheduler) {
await scheduler.yield();
} else {
await new Promise(r => setTimeout(r, 0));
}
runExpensiveFilter(); // the slow part
});
scheduler.yield() is not in every browser yet, hence the fallback. It won’t make the expensive function faster. It makes the interaction feel faster, which is what INP is measuring in the first place.
What I’d do this week
Ten thousand large sites is the right sample to learn from and the wrong one to copy blindly. Use it to decide what to measure, then measure your own.
Here’s the afternoon plan. Add the attribution snippet above to one production site and let it run for a few days. Look at the 75th percentile of TTFB, load delay and render delay, and fix the biggest one. Then split the same data by browser engine and check whether Safari on iPhone is worse than Chrome for your audience. If you’d like to see the sort of client work where I do this kind of audit, my portfolio has examples. And if you’re curious about the raw data, open BEACON in BigQuery and run one query against the LCP histograms for a browser and country you care about. It takes about ten minutes and it will change what you look at first.