Cutting a Page’s Image Payload by 85%

Harshit Rajput
Harshit Rajput
7 min read
Cutting a Page’s Image Payload by 85%

Image optimisation is the least glamorous performance work and usually the highest-yield. On our own site it took 1,834 KB of images down to 316 KB — an 85% reduction — without touching the design.

Then we had to give some of it back, for a reason worth writing down.

The Actual Problem Is Not Compression

Every image on the page was a raw <img> tag. That means the browser downloads exactly one file, at one size, regardless of the space it renders into.

Measured against how they actually displayed:

  • Portfolio images: 1500px wide sources rendering into ~400px cards
  • Slider images: 945px wide sources rendering into 190–350px tiles
  • App mockup: 2520px wide rendering into a ~700px column

Three to five times more pixels than needed, on every single load. No compression setting fixes that. The file is simply the wrong size.

Step One: Let The Browser Choose

Moving to next/image with a correct sizes attribute lets the browser pick from a generated set of widths. The gain was immediate and large.

But sizes is where this goes wrong, so it is worth being careful.

Deriving sizes, not guessing them

Our first pass set the service cards to 360px. Measuring the actual layout — a container capped at 1440px, three columns, minus gaps and padding — the real slot is about 417px.

Declaring 360 means the browser picks the 384w variant and upscales it. On a smooth gradient, that is visible.

The tell was in the numbers, and we nearly missed it:

javascript
ServiceBGDark.webp   77 KB  →  1.2 KB   (-98%)

A 98% saving on a gradient is not a win. It is an image being served far too small. Treat an unusually large saving as a bug report.

Derive each sizes value from the container arithmetic:

javascript
sizes="(max-width: 640px) 92vw,
       (max-width: 1024px) 46vw,
       (max-width: 1727px) 28vw,
       420px"

Slightly over-declaring is safe — the browser picks a larger variant. Under-declaring produces upscaling, which is the one outcome you cannot recover from.

Step Two: The Part Nobody Warns You About

With sizing fixed, one image still looked wrong. The dark gradient behind our service cards had gone blotchy — smeared patches instead of the fine grain in the original.

We decoded the served bytes and compared them against the source at true display width. The cause was not the dimensions. It was AVIF.

Those backgrounds are dark, low-contrast gradients with film grain baked in. AV1's psychovisual model classifies fine grain as noise and discards it, substituting smooth low-frequency blobs. That is precisely how it achieves such small files on this kind of image — it is throwing away the texture.

We ruled out the obvious fixes by testing rather than assuming:

  • Reproduced at w=640 and w=1080 — not a resolution problem
  • Reproduced at quality 85 and 100 — not a quality-budget problem

WebP at the same width preserved the grain. So the codec was the problem, not the settings.

The trade

Dropping AVIF site-wide cost, measured across all 18 optimised images:

javascript
AVIF total   300 KB
WebP total   348 KB   (+48 KB, +16%)

We took it. A 16% byte regression is worth avoiding a visible rendering defect, and it beat the alternative of serving one 328 KB background unoptimised.

The reasoning went into next.config.mjs as a comment, because formats: ['image/webp'] looks like an oversight to the next person otherwise.

Step Three: Know When To Stop

Not every image benefits. Our client logo strip is five PNGs of 2–4 KB, rendering about 120px wide. Optimised:

javascript
Logo_5.png   2 KB  →  3.0 KB

Every AVIF variant came out larger than the source, plus an optimiser round-trip. Those stayed as plain <img> tags, documented at the call site so nobody "fixes" it later.

The same logic applies to SVGs. The optimiser passes them through unchanged, so routing icons and logos through it buys a request and no bytes. We left the ESLint rule enabled — so a genuinely new raster image still gets flagged — and recorded why the remaining warnings are deliberate.

The Layout Shift Nobody Sees

While measuring bytes we found a separate defect that costs nothing to fix and hurts Cumulative Layout Shift directly.

Our logo declared width={160} height={40} — a 4:1 ratio. The actual artwork is 220×24, roughly 9:1. The browser reserves space using the declared ratio, then the real image loads and the box collapses.

In the footer it was worse: 1440×200 declared for the same 9:1 logo meant that at 1200px wide the browser reserved 167px of height for something 131px tall — a 36px jump on every page, since the footer is site-wide.

Nothing errors. The image looks fine once loaded. The only symptom is content moving under the reader's thumb.

Worth checking every hardcoded width/height pair against the real file dimensions. It takes minutes and it is the cheapest CLS fix available.

How To Audit Your Own

Four steps, in order:

  1. List every image with its source dimensions and its real rendered size. The gap between those two numbers is your opportunity, and it is usually larger than you expect.
  2. **Derive sizes from container arithmetic**, not intuition. Write down the container width, subtract gaps and padding, divide by columns.
  3. Look at the output. Decode what the server actually returned and compare it against the source at display size. A byte count cannot tell you the texture is gone.
  4. Check the declared dimensions match the files. As above — free, and it fixes real layout shift.

What The Numbers Actually Were

javascript
converted photos (18):  1,818 KB  →  348 KB   (-81%)
logos left as <img>:       16 KB  →   16 KB
page raster total:      1,834 KB  →  364 KB   (-80%)

Note the direction of travel across our three attempts: -85% (blurry) → -83% (smudged) → -80% (correct). Each correction cost bytes and bought fidelity.

That is the honest shape of this work. The biggest number is rarely the right answer, and a payload chart cannot tell you whether the image still looks like the designer intended. You have to open it.

More Articles