From 85 to 100 on mobile: how we found the real speed bottleneck (and it wasn't the images)
Everyone fixes a poor speed score by compressing images. Most of the time it's the wrong answer. How to read the Lighthouse breakdown to find the real bottleneck — render delay, not bytes — step by step, on the NovaFit homepage.

When a site fails Google's speed test, everyone's first reflex — many developers included — is the same: "compress the images". It's the obvious answer. It's also, most of the time, the wrong one.
The NovaFit homepage — the Câmpina fitness club whose site we rebuilt on Next.js — had a mobile PageSpeed score of 85 and an LCP of ~3.9 seconds (yellow, i.e. "you have a problem"). At first I did exactly what you'd do: I lowered the background image quality. The score didn't move a single point. That's the classic signal that you're treating the wrong symptom — and the moment most people start compressing even harder, getting nothing.
An hour later, the homepage had 100 across all four categories — Performance, Accessibility, Best Practices, SEO — with LCP down from 3.9s to 1.4s. This article shows exactly how — and, more importantly, why the move that mattered wasn't the one you'd have guessed.
The symptom that fools you
The NovaFit homepage hero is "heavy" by design: a full-screen dark background, a large pink ring over it, an athlete cutout and a huge headline — four layers composited into a single image. Exactly the kind of section where instinct says "too much image, that's why it's slow".
Except instinct isn't a measurement. And when I cut the image quality and the score stayed put, it became clear: the bottleneck is elsewhere, and every minute spent on compression is a minute wasted.
The rule that changes everything: measure, don't guess
Instead of assuming what was slow, I pulled from the Lighthouse report exactly which element is the "largest visible content" (LCP — Largest Contentful Paint, the metric Google penalizes hardest) and what its delay is made of. Two findings settled everything.
First: the LCP element was indeed the hero background — the image covering the whole screen, so the largest painted area. So far, instinct was right.
The second thing turned it all around. The LCP time breakdown looked like this:
| LCP sub-part | Duration |
|---|---|
| Time to first byte | 0 ms |
| Load delay | 100 ms |
| Image download | 270 ms |
| Render delay | 900 ms |
The image downloaded in 270 milliseconds — fast. But it was painted on screen only 900 ms later. Translation: the browser had the image in hand and sat for nearly a second unable to paint it. Compression attacks those 270 ms — which were already small. The real bottleneck was the 900 ms of render delay.
And the report said why too: at the top of the issues list, "render-blocking resources" — the CSS file forced the browser to wait ~700 ms before drawing anything.
You can't see this with the naked eye, and you can't guess it. You read it.
What I actually changed
1. I sized the LCP image by decode cost, not by kilobytes
The background is a 1920×1080 landscape image, but on a phone it's displayed "covering" a portrait screen — so already scaled up to fill the height. On a modern phone, the browser requested a bitmap over 1200 px wide, which the CPU — under Google's test conditions (slow-phone emulation) — decoded expensively.
Being a dark background at 60% opacity, I could request a much smaller version and lower quality with no visible difference — but with decode and transfer several times lighter. On a decorative background nobody sees the difference; the CPU does.
2. I removed a full-screen graphics layer
The background had a subtle parallax effect running on GPU acceleration (will-change). The problem: parallax only moves with the mouse — on a phone it's completely useless — yet it still occupied a full-screen graphics layer the browser had to prepare before painting. I removed it from the background (kept it on the small foreground elements, where it actually shows). Straight off those 900 ms.
3. The big lever: CSS delivered with the page
The main cause of the delay was render-blocking CSS. Normally the browser downloads the page, finds a link to the stylesheet inside it, makes an extra trip to fetch it, and only then draws. I enabled delivering the styles inline in the page, instead of a separate file: the styles arrive with the content, and the browser can paint immediately.
I verified on the build that it works right: the page ships with styles included and zero render-blocking CSS files. The technique has a narrow sweet spot — small styles (we use an "atomic" system that generates only the classes actually used) and mostly first-time visitors. It's exactly this case; on a site with large CSS and lots of returning traffic, it would be the wrong choice.
The trap that swallows optimizations silently
Worth stating separately, because it's the kind of detail that keeps you stuck for hours. The framework used (Next.js, new version) has a quirk: it silently ignores any image quality you haven't explicitly declared in an allowed list. You set "quality 45", get no error, and the image stays on its default value. So half your optimizations seem to "do nothing" — the exact symptom the whole story started from.
Once the list was declared correctly, the changes started to count. Without that step, you'd have sworn image optimization doesn't help — and you'd have been right, for the wrong reason.
Bonus: accessibility, also on the numbers
The same discipline solved the last yellow point — accessibility, stuck at 96. Lighthouse flagged one thing: in the gallery section, the grey secondary text on a light-grey background had a contrast ratio of 4.13:1 — below the 4.5:1 minimum required by the WCAG standard. Not "the text is a bit faint", but an exact number, below an exact threshold.
The fix: a single color value, darkened by a few percent, just enough to reach 4.74:1 — it stays discreet secondary text, but becomes legible for everyone. Accessibility went to 100. Same approach: not "tweak something by eye", but read exactly what the tool reports and fix exactly that much.
What the numbers confirm, step by step
The nicest thing about this story is that the progress, measured step by step, confirms exactly what the data said. Every intervention was re-measured on the live site:
| Moment | Mobile score | LCP | What shipped |
|---|---|---|---|
| Start | 85 | 3.9s | starting point |
| Step 1 | 90 | 3.6s | image sized for decode + useless GPU layer removed |
| Step 2 | 100 | 1.4s | CSS delivered inline (off the critical path) |
The comparison between steps is the useful part. The image optimizations — exactly what you'd instinctively reach for — brought 5 points and moved LCP only from 3.9 to 3.6s. Moving to render delay (inline CSS) brought 10 points and cut LCP from 3.6s to 1.4s.
The size ratio between the two jumps is the conclusion itself: the problem wasn't the bytes, it was the moment of paint. If the work had stopped at "I compressed the images", the score would have stayed at 90, with an LCP still in the yellow.
Final score — 100 across all categories, on mobile and desktop:
- Mobile: 100 · 100 · 100 · 100 — Performance · Accessibility · Best Practices · SEO FCP 1.0s · LCP ~1.4s · Speed Index 2.2s · TBT 70 ms · CLS 0 (emulated Moto G, slow 4G)
- Desktop: 100 · 100 · 100 · 100 FCP 0.3s · LCP 0.5s · Speed Index 0.5s · TBT 10 ms · CLS 0
- Every change committed atomically, with a clean build verified before going live
How it was done, technically (for the curious)
What matters here isn't how many lines of code changed — few did — but the right order of moves: diagnosis from real data first, intervention only after. Concretely:
- No change was started until the report showed where the bottleneck was. The first hypothesis ("it's the image") was tested and disproven by data, not intuition.
- Every intervention was verified at the source: that the image really serves smaller, that the CSS file really disappears from the critical path — checked on the build, not assumed.
- Disciplined git: atomic commits, one fix per commit, messages that explain why, nothing live without a clean build — so each step is independently reversible.
What stayed human: the "how far do we compress without it showing" call, the judgment that a 60%-opacity background tolerates low quality, and every "ship to production".
The same method, step by step, applies to other projects too: from 74 to 97 on mobile (where the bottleneck was analytics, not images), a 56-second LCP brought back under 2 and a 75-to-99 Lighthouse guide with the classic measurement pitfalls.
FAQ
Why wasn't compressing the images enough? Because the image download was already quick (270 ms). The real bottleneck was ~900 ms during which the browser had the image but couldn't paint it — because of render-blocking CSS. Compression doesn't touch that part. That's why "compress once more" didn't move the score.
How do you know the real problem instead of guessing? From Lighthouse's LCP breakdown, which says exactly how much was lost to download vs. paint, plus the render-blocking resources list. The measurement decides the direction; instinct only generates hypotheses to test.
Is the lower image quality noticeable? On a dark, decorative background at 60% opacity — no. That's exactly why I chose aggressive compression there and kept quality high on the clear foreground subjects. Good performance optimization is invisible to the visitor.
What do you do when a score is stuck below threshold? The same process as in this article: diagnosis from data (the metric breakdown, not the impression), intervention on the real cause, re-measurement after each step. Not "compress something else and hope". The report sets the direction, not intuition — which is why it repeats on any site, not just this one.
Got a site that "works" but fails Google's speed test? Get in touch — we'll tell you what's slowing it down, with the report in front of us. See our SEO / AI-SEO audit & fix service too.