Skip to content
All posts
4 min read

Chasing the Lighthouse score (and learning which numbers to ignore)

I dragged this portfolio to a perfect 100, and the report still yelled at me. The wins were boring and real; the flags that stuck around were a trap dressed up as a metric.

Semmy Verdonschot
Semmy VerdonschotFull-Stack Engineer
  • #performance
  • #web
  • #nextjs

I told myself I'd "just check the Lighthouse score" on this site. Three hours later I was reading about device pixel ratios and arguing with a number. If you've ever opened that report and felt the itch to make everything green, this one's for you, because the useful part wasn't getting to a high score. It was learning which flags deserve your attention and which are quietly lying to you.

The wins were boring

Almost everything that actually moved the needle was unglamorous.

The biggest jump came from images I'd barely thought about. The video thumbnails on the home page were YouTube's maxresdefault (1280×720) served at quality 85. They're small, decorative cards. So I dropped them to quality 65 and let them serve at around 480px instead of the 750px variant the browser was reaching for. That one change took the "image delivery" savings from 174 KiB down to about 12 KiB. No one will ever notice the difference looking at a 360px-wide thumbnail.

The book covers were a different kind of boring. I was pulling them live from Open Library through Next's image optimizer, and the optimizer kept timing out: ERR_TIMED_OUT in the console, three times. Open Library's cover server is slow and moody, and every cold request had to go fetch from it before optimizing. The fix wasn't clever: download the five covers once, drop them in /public, and serve them locally. The optimizer reads a fast local file now and the timeouts are gone.

Then the fonts. I was preloading the monospace font even though nothing above the fold uses it. On a throttled connection that's just one more file racing the hero image for the first bytes. Stop preloading it, and the hero gets a little more room.

None of this is a trick. It's reading what's actually on the wire and removing the silly parts.

The hero, and a decision that looks wrong

The Largest Contentful Paint element is my photo at the top. The obvious move is to let Next optimize it: AVIF, a responsive srcset, the works. I did exactly that, and the cold LCP on emulated Slow 4G got worse.

Here's why: routing the hero through /_next/image adds an optimizer round-trip. On a cold run that's an extra hop before the most important pixel on the page can paint. The image is already a small, right-sized WebP, so I reverted to serving it directly with a priority preload that hits the CDN immediately. I even measured an AVIF version to be sure I wasn't being stubborn, and at the same quality it came out larger than the WebP. Sometimes the optimization you skip is the optimization.

That trade shows up as an "Improve image delivery" flag worth 10 KiB. I'm keeping it.

The numbers that are lying to you

Here's the part I wish I'd internalised earlier: a lot of what Lighthouse flags is tagged Unscored. It shows up in the report, it looks red and urgent, and it does not touch your score at all.

  • "Reduce unused JavaScript, 26 KiB." I went and inspected the chunk. It's react-dom. The runtime of the framework the whole site is built on. The "unused" bytes are code that runs the moment anything becomes interactive. You cannot delete React from a React app to please a gauge.
  • "Avoid long main-thread tasks." It's hydration. The report literally says these numbers don't directly affect the performance score.
  • "This image is larger than it needs to be (720×540 for a 364×273 display)." This is the sneakiest one. Lighthouse is comparing against a 1× screen. Almost every phone is 2× or 3×, where a 720px image for a 360px slot is exactly right. Shrink it to make the flag happy and you've shipped a blurry photo to every real visitor.

That last one is the whole lesson in miniature. The metric is optimising for a device almost nobody uses.

Where I landed

A perfect 100: performance, accessibility, best practices, SEO, all green. The single biggest jump came from the part that looked wrong. Serving the hero directly and dropping the preload on a font the first paint doesn't use took Largest Contentful Paint from 2.8s down to 1.0s on throttled mobile. The boring image and cover fixes did the rest.

And here's the thing that made the whole exercise click. Under that perfect score, the report still flagged "reduce unused JavaScript" and a long main-thread task, red and urgent as ever. A 100 doesn't make them disappear, because they were never the problem; they're unscored framework noise. The points I deliberately didn't chase, like shrinking my own face to satisfy a 1× metric or stripping the serif preload for a flash of fallback text, would only have traded real, visible quality for numbers that don't count.

The takeaway

Performance work is worth doing: right-size your images, kill the slow third-party fetch, don't preload what the first paint doesn't need. Do all of that. But treat the score as a smoke alarm, not a high score to beat. Learn which flags are real and which are vanity, find out what your users' devices actually are, and once the genuine wins are in, have the discipline to leave the rest alone. The green number is for you; the fast, sharp page is for them.

Semmy Verdonschot

Written by

Semmy Verdonschot

Full-Stack Engineer from Limburg, Netherlands, writing on secure, modern web development and the craft of shipping software.