Skip to the notebook
Maxsash Studio
← Back to the notebookField note 005 / Note

Performance & hosting

What PageSpeed saw in the sea

Which numbers to worry about, which to ignore, and the header that told me my server was on the wrong continent.

· Note · 6 min read
05A lab report read slowly: first byte, render delay, and the number that would not reproduce.
h(x,z,t)t = 0 sA SURFACE MADE OF RELATIONSHIPSSIX SUPERPOSED WAVES

I ran my home page through PageSpeed Insights for the first time and got this.

Lab resultMobileDesktop
Performance5762
First Contentful Paint1.6 s0.4 s
Largest Contentful Paint3.3 s0.7 s
Total Blocking Time3,160 ms27,700 ms
Cumulative Layout Shift00
Speed Index5.7 s3.1 s

Accessibility was 97, Best Practices 100, SEO 100. The "real users" section said No Data, because the site does not have enough real traffic yet for Chrome's field data.

That last point matters. Google's page-experience signals come from field data. This report was a lab test, one run on a machine I do not control. It is useful for finding things, and a poor thing to be anxious about.

Where the score went

Look at what each metric contributes. On desktop, paint timings and layout shift were fine. Nearly all the missing points were one metric: Total Blocking Time, worth about a third of the score. 27.7 seconds, with 33.9 seconds of main-thread work.

My page runs a WebGL scene continuously. My understanding, which I have not checked in Lighthouse's source, is that it keeps measuring while the main thread is busy, and mine never goes quiet. On a machine with no GPU, WebGL falls back to a software renderer, and that is slow enough to keep the browser busy for the whole test. I wrote about that separately; the short version is one line of code that makes software-rendered browsers show a static plate instead.

What I want to talk about here is what I did not know, and how I checked.

Mobile 57 / Desktop 62 / TBT 27.7 s

I could not reproduce it

I replayed the situation locally: headless Chrome, no GPU, a phone-sized screen, CPU slowed 4×. The blocking time came out at 103 ms on desktop and 151 ms on mobile, not 27 seconds. My laptop's software renderer is much faster than whatever PageSpeed's worker has.

So I could not prove the cause. I could show the mechanism (the scene was the main work, and turning it off dropped mobile blocking from 151 ms to 21 ms) and fix it, but the proof will be the next report. That is an uncomfortable position, and it is the honest one.

103 ms desktop / 151 ms mobile / Locally

First byte: read the headers

One run's LCP breakdown said time to first byte 820 ms and element render delay 780 ms. The first number was the larger, and it was a surprise: my page is simple.

I asked my own site for its response headers:

cache-control: private, no-cache, no-store, max-age=0, must-revalidate
x-vercel-cache: MISS
x-vercel-id: bom1::iad1::kwxnc-...

Two things. The page is never cached, which I knew: the server picks a random sea for every visit. And x-vercel-id lists the regions that handled the request: bom1 is the edge that received it, in Mumbai, and iad1 is where the function ran, in Washington, D.C. Three requests from India took 1.8, 0.57 and 0.38 seconds to the first byte.

I would not have found that from the PageSpeed report. It does not name regions. The header did. If your site is dynamic, check where your function runs against where your readers are. I am based in India, so three days later I moved the function to Mumbai: the header now reads bom1::bom1, and in the next PageSpeed runs the first byte's share of LCP fell to 290–360 ms, with no document-latency warning.

bom1 edge / iad1 function / 0.4–1.8 s

Render delay: not my code

With the first byte out of the way I wanted to see what the 780 ms was. I replayed a Slow-4G load locally (1.6 Mbps, 150 ms latency, 4× CPU) with no server latency at all. The heading painted at about 0.9 seconds. During that time the page was pulling two blocking stylesheets, a 137 KB (gzipped) document, and three preloaded fonts totalling 206 KB, all over the same thin link. That is a bandwidth story, not a code story. I looked at trimming the font axes and found that every one is in use, so I left it.

Slow 4G / 206 KB of fonts / 0.9 s

"Forced reflow: 168 ms"

Lighthouse flagged a forced reflow. I knew my code reads layout sizes when it starts, so I expected a real problem. I measured it instead.

With the browser's own metrics, layout took about 130 ms at 4× throttling, close to the report's 168 ms. Then I tried the obvious cure, content-visibility: auto on the sections below the fold, so the browser could skip them. Three runs each:

Layout timeBaselineWith content-visibility
three runs, ms143, 131, 129130, 126, 127

Under ten percent. The expensive layout was the hero itself, not the sections below, and it had to happen once anyway. I did not ship it. A change that saves a few milliseconds and touches find-in-page, anchors and scroll behaviour is not a good trade.

About 130 ms / Under ten percent

The one fix that was plain old accessibility

The contrast failure turned out to be a pill that says "You are sailing this sea". I had dimmed it with opacity: 0.65. Opacity quietly lowers contrast, and it is easy to forget because the colour you wrote is fine. I swapped it for a muted text colour, and measured 5.2:1 in day mode and 7.8:1 at night, above the 4.5:1 requirement.

The same report flagged two links with identical text ("Read the case study") that go to different places. A screen-reader user hears them as the same. I added the project name to each as visually hidden text.

5.2:1 day / 7.8:1 night

After the fixes

The next report, after the static plate for software rendering and the move to Mumbai:

Lab resultDesktop beforeDesktop afterMobile beforeMobile after
Performance62995792
Total Blocking Time27,700 ms20 ms3,160 ms100 ms

One run each, on a lab machine, so I read it as "the lead was right", not as a measurement of real visitors. Those numbers will come from field data once there is enough of it.

62 → 99 desktop / 57 → 92 mobile / One run each

What I would do again

  • Treat a lab report as a list of leads, not a verdict.
  • When you cannot reproduce a number, say so, and fix the mechanism you can show.
  • Read your response headers. x-vercel-id told me more than the report did.
  • Measure the cure before you ship it. My best guess for the reflow was wrong.

Leads / Headers / Measure first

Treat the lab as a list of leads, not a verdict.