← Writing

Image weight, the silent conversion killer

Someone on your team takes a photo of the new inventory, uploads it to the site, and it looks great. On the office Wi-Fi, on a desktop monitor, it loads instantly.

On a phone, in a parking lot, on two bars of LTE, that same page takes eleven seconds and the visitor is gone before the picture arrives. They don't file a complaint. They just leave, and nothing in your analytics tells you why.

The arithmetic of one photo

A current phone camera produces something around 4032 by 3024 pixels — about 12 megapixels — and a file of two to five megabytes. Your page displays it in a card 800 pixels wide, then a hair over 400 on a phone.

The browser doesn't know that until it has the file. So it downloads all four megabytes over the cell connection, decodes it, and then scales it down for display. And decoding isn't free either: a decoded bitmap costs roughly four bytes per pixel, so that one photo occupies about 49 MB of memory while the browser resizes it into a thumbnail. Put six of them on a category page and a mid-range Android starts swapping and stuttering.

The same photo, resized to 1600 pixels wide and encoded as WebP at quality 75, is usually 100 to 200 KB. Visually identical in the box it appears in. That's a twenty-to-one reduction from doing nothing except sending the right size.

Serve the size that's actually needed

Three things, in order of payoff.

Resize on upload, not on request. Every image that enters the site should be processed once, at upload, into a small set of renditions — say 480, 960, and 1600 pixels wide — in a modern format. Do it at upload and the cost is paid once by one person, not on every visit by every visitor forever. Strip EXIF while you're in there: phone photos carry GPS coordinates, and a home-based business publishing its own address in image metadata is a real problem.

Let the browser choose. That's what srcset is for:

<img src="/img/bench-960.webp"
     srcset="/img/bench-480.webp 480w,
             /img/bench-960.webp 960w,
             /img/bench-1600.webp 1600w"
     sizes="(max-width: 700px) 100vw, 700px"
     width="1600" height="1200" alt="Workbench in the shop">

The browser picks a rendition using the viewport, the layout, and the device pixel ratio, and downloads exactly one of them.

Always set width and height. Those two attributes let the browser reserve the right space before the image arrives, so text doesn't jump down the page mid-read. That jump is Cumulative Layout Shift, and it's the reason people tap the wrong button. The attributes cost nothing and CSS still controls the real display size.

The mobile trick that doesn't work

Here's the one that costs people a week. You want the big hero photo on desktop but not on phones, so you add:

@media (max-width: 700px) { .hero-img { display: none; } }

The image disappears. The download does not. An <img> element in the markup gets fetched by the browser's preload scanner before your stylesheet has any say in the matter — the phone pays for every byte of a photo it will never render. You've made the page slower on mobile than it was before, while the visual result looks exactly like success.

The fix is to make the decision where the browser can act on it: srcset and sizes so the phone fetches a small rendition, or <picture> with a <source media="...">, or simply not putting the element in the markup for small screens. (A CSS background-image on a hidden element genuinely isn't fetched — which is why the two behave differently and why the display: none habit spreads.)

The same class of mistake hides in the hero image the other direction: don't put loading="lazy" on it. The hero is almost always the element the browser measures for Largest Contentful Paint, and lazy-loading defers the exact thing being timed. Lazy-load everything below the fold; mark the hero fetchpriority="high" and let it race.

What to do this week

  1. Open your slowest page, load it on a phone with the cache empty, and look at the transferred size. Anything over two megabytes is a photo problem.
  2. Find the largest three images on the site. Those are almost certainly full-resolution uploads.
  3. Fix it at the source — resize and convert on upload — so the problem stops recurring the next time someone adds a product.
  4. Re-encode the existing library once. It's a batch job, not a redesign.

A site that only ever ships the pixels it displays doesn't need a performance project later. It just stays fast, including for the customer standing in a parking lot deciding whether to wait.

Processing images properly on upload is one of the things you get by default with our hosting.


Need this kind of thinking applied to your own setup? Get in touch →