Skip to content
Engineering · 4 min read

Images, not JavaScript, are what your pages weigh

The claim On a typical content or commerce site, images account for the majority of transferred bytes — usually 55% to 75% — and the fix is a build step you configure once, not a f...

A Written by Administrator
Images, not JavaScript, are what your pages weigh

The claim

On a typical content or commerce site, images account for the majority of transferred bytes — usually 55% to 75% — and the fix is a build step you configure once, not a feature you rent from a vendor. Teams spend months arguing about framework bundle size while shipping a 2.4 MB hero photograph that could have been 140 kB with no visible difference.

Measure your own split first

Open the network panel, filter by type, and read the totals. Or from a terminal, using your own build output:

du -sh public/images public/assets/js
find public/images -type f -size +500k | head -20

That second command is usually the whole story. If it returns twenty files, you have twenty problems that a single afternoon will fix.

The three changes, in order of return

1. Encode to AVIF with a JPEG fallback. A 3000-pixel-wide photograph at JPEG quality 85 lands around 1.2 MB. The same image as AVIF at quality 50 is typically 120 to 200 kB and is indistinguishable at normal viewing distance. WebP sits in between at roughly 300 kB and has universal support, so it makes a reasonable middle tier.

npx sharp-cli -i hero.jpg -o hero-1600.avif resize 1600 --format avif --quality 50
npx sharp-cli -i hero.jpg -o hero-1600.webp resize 1600 --format webp --quality 78

2. Stop sending desktop images to phones. This is the change with the largest effect and the one most often skipped. A phone with a 390-pixel-wide viewport at 2x needs an 780-pixel image. Sending it your 2400-pixel original wastes about 85% of the bytes.

<img
  src="hero-1200.jpg"
  srcset="hero-600.avif 600w, hero-1200.avif 1200w, hero-2000.avif 2000w"
  sizes="(max-width: 700px) 100vw, 60vw"
  width="1200" height="675"
  alt="Warehouse floor with racking">

The sizes attribute is the part people get wrong. It describes the layout width of the image, not the viewport. If it is missing, the browser assumes 100vw and picks the largest candidate, which undoes the whole exercise.

3. Set explicit width and height on every image. Not for bytes — for layout stability. Without them the browser cannot reserve space, so the page reflows when each image arrives. This is the dominant cause of poor cumulative layout shift scores and it is free to fix.

Loading behaviour matters as much as size

Two attributes, applied to different images:

  • loading="lazy" on everything below the fold. On a category page with 40 product thumbnails, this alone can cut initial transfer by 80%, because most visitors never scroll past the first six.
  • fetchpriority="high" on the one image that is the largest element above the fold, and never loading="lazy" on it. Lazy-loading your hero image delays it until layout completes, which directly worsens the metric you were trying to improve.

Two mistakes that undo the work

Re-encoding an already-compressed source. Optimise from the original camera file or the designer's export, not from the JPEG that has already been through a compression pass. Each round trip through a lossy codec discards detail permanently, and the second pass buys far fewer bytes than the first.

Using a photographic codec for flat graphics. A logo, a chart, or a screenshot of an interface compresses far better as SVG or PNG than as AVIF, and stays crisp at any zoom level. The rule of thumb is simple: if the image has large areas of a single colour and hard edges, it is not a photograph and should not be encoded as one.

Cache aggressively by fingerprinting

Once an image is optimised, it should be downloaded exactly once per visitor, ever. Put a content hash in the filename and serve it with a one-year immutable header:

location ~* \.(avif|webp|jpg|png)$ {
    add_header Cache-Control "public, max-age=31536000, immutable";
}

The immutable directive tells the browser not to revalidate even on a hard refresh. This is safe only with fingerprinted filenames — if you serve logo.png with that header, you cannot change the logo for a year.

Where a paid image service earns its money

Build-time optimisation covers images your team controls. It does not cover user-uploaded content: a marketplace where sellers upload photographs from their phones will receive 8 MB files with the camera's full metadata attached. For that, an on-the-fly resizing service — or a small worker of your own using sharp behind a cache — is the right answer, because you cannot rebuild the site every time someone lists a chair.

What to expect

A content page carrying 3.1 MB of images typically lands between 350 and 600 kB after AVIF encoding, responsive sizes, and lazy loading. On the 1.6 Mbps connection from our earlier arithmetic, that is roughly fifteen seconds of transfer reduced to under three. No framework change, no rewrite, no new monthly bill — one build step and five attributes on a tag that has been in HTML since 1993.

#web performance #images #frontend #caching

Keep reading