DEV Community

Daniel Pertu
Daniel Pertu

Posted on

Our 72-page site contains 73 images, and 68 of them are the same 606-byte mug

I counted every <img> element served by pub-trivia.app. All 72 URLs in the sitemap, the HTML as a crawler receives it.

images per page: {1: 68, 0: 3, 2: 1}
Enter fullscreen mode Exit fullscreen mode

73 image elements on 72 pages. 68 pages have exactly one. Three have none. One has two.

For a marketing site that is either admirable restraint or a warning, and the interesting part is what the single image turns out to be.

The one

It is the logo in the header, and here is how it ships:

<img alt="PubTrivia logo"
     loading="lazy"
     width="26" height="26"
     decoding="async"
     data-nimg="1"
     src="/logo.svg?dpl=dpl_EKzaTAmQUBUdseoodG43waJqVSuS">
Enter fullscreen mode Exit fullscreen mode

606 bytes of SVG. A mug. Three things about that tag are worth a paragraph each.

loading="lazy", on the element at the very top of every page. That is the next/image default, and the default is right for almost everything, which is exactly why nobody notices it on the one element where it is wrong. The browser will not request it until layout tells it the element is near the viewport, which costs a round trip on an asset that is smaller than the tag describing it. There is one opt-out, priority, and the logo does not use it.

It does not go through the image optimiser. data-nimg="1" says the Image component rendered it, but the src is the raw file, not /_next/image. Next refuses to optimise SVG unless you explicitly allow it, because an SVG is a document that can contain script, and passing one through an optimiser that serves it from your origin is a decision rather than a default. This is the correct behaviour and I am glad it is loud about it.

The alt text. Look at the whole link:

<a class="..." href="/">
  <img alt="PubTrivia logo" ...>PubTrivia
</a>
Enter fullscreen mode Exit fullscreen mode

The word PubTrivia is inside the anchor, in text, immediately after an image whose alt text is "PubTrivia logo". A screen reader announces the link as "PubTrivia logo PubTrivia". The alt should be empty: the image is decorative here because the brand name is already present as text, and "logo" in alt text is noise even when the image is the only content, since nobody needs to be told what kind of picture it is.

One tag, three things, all of them the kind that survive for months because the page looks fine.

The three with none

The legal documents. They render their own header, a white bar with a text wordmark and no image at all, which is why the privacy policy is the only part of the site that contains zero image elements. They are also, separately, the only three pages rendered in a light theme inside an otherwise dark site, but that is a different story.

The one with two

The homepage. The second image is the hero background, and it is the only priority image in the entire codebase:

grep -rn "priority" app components   ->   one usage, app/page.tsx
Enter fullscreen mode Exit fullscreen mode

Which means it is also the source of the only rel="preload" as="image" tag on the whole site. 71 of our 72 pages preload no image at all, which is correct, because they contain one 606-byte SVG each.

The hero is served fill, with sizes="(max-width: 1320px) 100vw, 1320px" and a srcSet running from 640 to 3840 wide.

The optimiser nobody configured

Here is the part I did not expect. next.config.mjs has no images block. Not a trimmed one, not a commented-out one: the key is absent. So every width and every quality value comes from the framework defaults, and those defaults are an allowlist that is actively enforced in production.

Try a width that is not on the list:

/_next/image?url=%2Fdemo-1.webp&w=1200&q=75   200  16597  image/jpeg
/_next/image?url=%2Fdemo-1.webp&w=1000&q=75   400     84  text/plain
/_next/image?url=%2Fdemo-1.webp&w=1200&q=50   400     84  text/plain
Enter fullscreen mode Exit fullscreen mode

w=1000 and q=50 are 400s. This is a good default and worth knowing about before you hand someone a hand-written /_next/image URL: the optimiser is not a general image resizing service pointed at your bucket, it is a cache with a fixed set of keys, and the set is the one you declared, or the one you did not.

The same URL, two answers

The other thing those numbers show, if you read them twice:

# no Accept header
/_next/image?url=%2Fdemo-1.webp&w=1200&q=75   16597 bytes  image/jpeg

# Accept: image/avif,image/webp,image/apng,*/*
/_next/image?url=%2Fdemo-1.webp&w=1200&q=75   10822 bytes  image/webp
Enter fullscreen mode Exit fullscreen mode

Same URL, same query string, 53 per cent more bytes for the client that did not say what it could read. At the top of the srcSet the gap is 110,319 bytes against 76,458.

That is content negotiation doing its job, and it is also why measuring your own image sizes with curl and no headers tells you a number no browser will ever receive. I have done this to myself before. The fix is one flag.

What escapes all of it

Two files in public/ never touch any of the above:

/demo-1.webp   1,769,688 bytes   (the hero, optimised down to ~10 KB in practice)
/demo-2.webp     953,385 bytes   (CSS background, served whole)
/demo-3.webp     915,548 bytes   (CSS background, served whole)
Enter fullscreen mode Exit fullscreen mode

demo-2 and demo-3 are set as backgroundImage on elements rather than rendered as <img>, which means the optimiser never sees them, the srcSet never happens, and both ship at full size to everybody who scrolls that far. The image census above counts zero of them, because they are not image elements. An audit that counts <img> tags will always tell you a site is lighter than it is.

That is the actual lesson of the whole exercise. next/image is opt-in per element, so the measurement that matters is not "are our images optimised", it is "which of our images are images".

Check any of it

# the one image on 68 pages
curl -s https://pub-trivia.app/faq | grep -o '<img[^>]*>'

# the allowlist, enforced
curl -sI "https://pub-trivia.app/_next/image?url=%2Fdemo-1.webp&w=1000&q=75" | head -1

# content negotiation
curl -s -o /dev/null -w '%{size_download} %{content_type}\n' \
  "https://pub-trivia.app/_next/image?url=%2Fdemo-1.webp&w=1200&q=75"
curl -s -o /dev/null -H 'Accept: image/webp,*/*' -w '%{size_download} %{content_type}\n' \
  "https://pub-trivia.app/_next/image?url=%2Fdemo-1.webp&w=1200&q=75"
Enter fullscreen mode Exit fullscreen mode

And if you would like to see the mug at a size where 606 bytes is doing real work, it is in the corner of every page at pub-trivia.app, where the free tier also happens to need no card.

Top comments (0)