All articles
·by · Manilla Services·Studiu de cazPerformanțăCore Web VitalsNext.jsOptimizare

2 MB of invisible content: the payload a page shipped without ever showing it

A homepage that shipped the full text of 462 patterns on every visit — 2 MB of data it never displayed. A case study on over-fetching in Next.js: how you weigh the served payload instead of guessing, why mobile paid for 2 MB it never used, and how you cut the invisible cargo from the page.

2 MB of invisible content: the payload a page shipped without ever showing it

It’s a trap instinct never sees: a site can be built cleanly, served statically, on a fast server — and still fail the speed test on mobile. Not because of what shows, but because of what gets shipped alongside it.

The Cipher — a public library of patterns, with 462 entries — had a PageSpeed score of 95 on desktop and 68 on mobile, with an LCP of 5.9 seconds (red, “you have a serious problem”). Same page, same code, same database. The difference between 95 and 68 wasn’t the code. It was bandwidth: on Google’s simulation of an average phone on slow 4G, something heavy downloaded before the page could breathe.

Instinct says “compress the images more.” But the homepage barely has images — it’s a flow of moving text over a dark background. So the brake was somewhere else. A few hours later, mobile was at 98, desktop at 100, and the LCP had dropped from 5.9s to 2.1s. This article shows exactly how — and why the decisive move wasn’t the one you’d think of.

The rule that changes everything: read what’s served, not what you think is served

Instead of assuming what was heavy, we simply requested the page the way a visitor receives it and weighed it. 2.2 MB. For a homepage with a title and a text flow, the number made no sense.

We searched the raw served content for a single word — the name of the field that holds the editorial body of each pattern. It appeared 462 times.

Meaning: on every visit, the homepage was shipping the full text of all 462 patterns — even though the page displays none of them. The front-facing flow shows only a short sentence each; the entire body has no business being there. It was, quite literally, invisible cargo: two megabytes the browser downloaded, held in memory, and never painted.

Why had they ended up there? Out of a “just in case it’s handy”: the data was passed to the component in full, in case the visitor opens a pattern’s detail window. Except that on mobile, opening a pattern takes you to its own page (prerendered, instant) — the detail window is desktop-only. The conclusion stung: mobile was downloading 2 MB it never used.

You don’t guess that. You weigh it.

What we changed, exactly

1. The page ships only what it displays; the rest comes on demand

We cut from the payload everything the homepage doesn’t render on load. What stays is strictly what it needs: the title, the hook sentence and the number of each pattern — just enough for the list and quick search to work. A pattern’s full body is fetched only when it’s actually needed: on a click on desktop, the detail window requests that single pattern, fresh, through a dedicated route (~0.2s). On mobile, where the click navigates to the page anyway, nothing extra is fetched.

The result, measured on the live page: 2.2 MB → 793 KB. Same page, same experience — just without the invisible cargo.

2. The database no longer gets read dozens of times for nothing

Along the way we hit a problem that was actually blocking the site’s build too: each of the 462 prerendered pages requested, at build time, the whole collection, two or three times per page. On a slightly slower database, the build ran into the 60-second-per-page timeout and never finished.

The fix has two layers: a request-level memoization (the two or three identical reads become a single one) and a “light” variant of the query that no longer carries the heavy text bodies the pattern page doesn’t need anyway. The build went from “never finishes” to green, with all 462 pages prerendered.

3. The images: a 1024 px logo for a 14 px badge

The only image served on every page was a credit logo — delivered at 1024×1024 pixels and displayed at 14 pixels. 96 KB for a dot the size of a fingernail. Resized to its real dimensions: 1.5 KB. No difference to the eye, 60 times lighter. Good performance optimization is invisible to the visitor; it only matters to the browser.

4. The fonts: half were for letters that never appear

The site is entirely in English, but for each font it also loaded the “Latin Extended” character set — diacritics and glyphs that appear nowhere in the text. Dropping the useless set → nearly half the font weight disappeared. On top of that, the browser target was moved to modern ones, which removed a layer of compatibility code for old browsers this audience doesn’t need.

The black-screen mystery: a clash of clocks, not an error

One symptom remained that Lighthouse punished harshly: an LCP render delay of ~2.4 seconds — on the filmstrip, a good second of black screen at the start. The site has a cinematic intro: a hero title holds for a few seconds, then fades into the text flow.

The cause turned out to be elegant and quiet. The hero disappeared on a CSS clock (a fixed timer), while the flow behind it appeared on a JavaScript clock (after the code loads and runs). On a fast phone, the two overlap nicely. On Google’s slow-phone simulation, JavaScript lags a few seconds — so the hero, driven by CSS, left before the flow, driven by JS, had a chance to appear. Between them: a black gap.

The fix respects the design intent exactly — a hero that holds, then fades into the flow — but moves the trigger: the hero stays visible until JavaScript confirms the flow is ready, and only then retreats. Because JS knows when the content is ready; CSS, running on a timer, doesn’t. The result: on a good connection, the same 2–3 seconds of hero; on a slow connection, the hero covers more — but never a black screen.

What the numbers confirm, step by step

Every intervention was re-measured on the live site. The comparison between steps is the useful part:

MomentMobile scoreLCPWhat was shipped
Start685.9sstarting point
Step 180~3.5spayload trimmed (2.2 MB → 793 KB) + images + database
Step 2982.1sblack screen fixed + fonts halved

The weight optimization — the invisible cargo pulled out of the page — produced the first big jump, from 68 to 80. And syncing the hero with the flow plus the fonts covered the rest of the way, up to 98, cutting the LCP below the green threshold. Neither the bytes alone nor the animation alone — but both, measured one at a time.

Final score:

How it was done, technically (for the curious)

What matters here isn’t how many lines changed — there are few — but the order of the moves: diagnose from what’s actually served first, intervene only after.

What stayed human: the decision of “what exactly the page needs on load vs. on demand,” the judgment that a decorative logo tolerates aggressive compression, and every “ship it to production.”

Frequently asked questions

Why was it fast on desktop and slow on mobile, with the same code? Because the brake was bandwidth, not processing. Desktop, on a good connection, swallows 2 MB without blinking. The phone, on slow 4G, feels every one of them. Same page, same files — it’s just that invisible weight only hits where the bandwidth is small.

Why wasn’t compressing the images enough? Because the homepage barely has images. The weight was data — the full text of 462 patterns shipped for nothing. Image compression had nothing to touch there. That’s why the usual reflex wouldn’t have moved the score.

How do you know the real problem instead of guessing? You weigh the served page and read what the weight is made of, plus the breakdown of the LCP metric (how much was lost on download vs. on paint). The measurement gives the direction; instinct only generates hypotheses to test.

Isn’t the detail window that loads “on demand” slower? It fetches a single pattern in ~0.2 seconds and immediately shows the title and hook while the rest fills in — with a visible indicator, not a blank screen. On mobile it doesn’t even apply: there you navigate to the pattern’s page, which is prerendered and opens instantly. The trade-off is invisible, and the gain — 2 MB pulled off every visitor — isn’t.

Was the black screen a real problem or only in Lighthouse? The root was real (a clash between a CSS timer and a JavaScript one), but Lighthouse, which simulates a very slow phone, amplified it until it became visible. Fixing it at the root makes it disappear both in the test and on real slow devices.

See also the The Cipher project — the public pattern library behind this case study.

From the same series: Why a beautiful site fails the speed test — the NO_LCP mystery — the hunt for the LCP element and the race to 91/100, on the sibling hub.


Got a site that “works” but fails the speed test on mobile? Get a quote — we’ll tell you what’s slowing it down, with the served weight in front of you, not assumed. See our services too.