← All writing
Craft · · 8 min

Let the browser measure it

The four attributes that matter more than any breakpoint list, and the one that finally lets the browser stop guessing.

Images Responsive Performance

Every image on the page came down at 1600px wide, on a phone, on a site built last year.

I was auditing it in April for something else entirely. Network panel open, sorted by size, and there was a column of identical numbers where the product thumbnails should have been. I have written that sentence before.

The pipeline was good, which is the part worth saying first. A handful of widths per image out of the build, AVIF with a JPEG behind it, every path correct, width and height on everything. Somebody had put real work into that. None of it had a sizes attribute.

sizes defaults to 100vw, so with nothing written the browser assumes each image will be as wide as the viewport and then multiplies by the device pixel ratio. A phone at 3x wants a bit over 1100 real pixels for a full-width image. The set goes 400, 800, 1600. So it takes the 1600, including for a thumbnail that renders at a third of the width of the screen.

Nothing looks wrong. The images are sharp, nothing is stretched, there’s no console error, and unless you open the network panel and go and look at what actually came down there is no symptom at all. That’s why it gets through a code review. That’s why it was still there a year after launch.

Same cause, same symptom, same one-line fix as it was in 2018. Everything around this changed in the meantime. That hasn’t moved an inch.

What actually got better

width and height came back from the dead. I used to strip them off, on the grounds that CSS does the sizing and the attributes are a relic. Then browsers started deriving an aspect ratio from the pair of them and holding the space before the file lands, and the two oldest attributes on the element are the biggest single fix for the thing where the page jumps. Put them on everything, always, with the real dimensions of the file. It’s the most valuable thing in this post and it has nothing to do with srcset.

loading="lazy" made the whole question smaller. An image that never gets fetched doesn’t need the right width, and one attribute in every template did more for the average page than years of me tuning breakpoint lists ever did. Not above the fold though, and there are a few places where it makes things worse.

fetchpriority="high" on the one image that matters. The browser deprioritizes images against stylesheets and scripts, which is right nearly everywhere and wrong for the hero, which is almost certainly your largest contentful paint. One attribute, and on a couple of sites it has been worth more than everything else I did that day.

And the complaint I ended that old post with, that three files per image regenerated by hand forever is not a thing a person keeps up with, has been comprehensively answered. Every framework has an image component now, image CDNs do the same job on the fly, and nobody hand-writes a srcset anymore. Which is precisely why nobody notices when the template writing them doesn’t write a sizes.

then and now
<!-- the old way -->
<img src="hero-800.jpg"
     srcset="hero-400.jpg 400w, hero-800.jpg 800w, hero-1600.jpg 1600w"
     sizes="(min-width: 60rem) 40rem, 100vw"
     alt="…">

<!-- what I write now -->
<img src="hero-800.jpg"
     srcset="hero-400.avif 400w, hero-800.avif 800w, hero-1600.avif 1600w"
     sizes="(min-width: 60rem) 40rem, 100vw"
     width="1600" height="900"
     alt="…"
     loading="eager" decoding="async" fetchpriority="high">
HTML

A stylesheet in the markup

The framing I’d still keep from back then is that srcset is you listing what you’ve got, while sizes is you telling the browser how wide the thing will end up on the page, because it has to choose before layout has happened and it would rather guess than wait.

And it’s a bad attribute because it’s a copy of your CSS, written in a second language, kept in a different file, held in step by hand. Change a container from 40rem to 44rem and every sizes on the site is quietly wrong and nothing anywhere tells you about it. It’s the only piece of layout information a modern site is still expected to keep outside the stylesheet.

I mitigated it instead of fixing it. Keep the value in the template that renders the component so there’s only one of it, and keep it loose, because a rough sizes survives a redesign and a precise one is a liability. I still do both of those. But there’s an actual fix now for a decent chunk of it.

Let it look

sizes="auto" tells the browser to skip the prediction and use the width the image actually got.

thumbnail.html
<img src="thumb-400.avif"
     srcset="thumb-400.avif 400w, thumb-800.avif 800w, thumb-1200.avif 1200w"
     sizes="auto, (min-width: 60rem) 12rem, 33vw"
     width="1200" height="800"
     alt="…"
     loading="lazy" decoding="async">
HTML

No media queries, no rem values, nothing to keep in step with anything. The layout is the source of truth, which it should have been the whole time.

It works because the image is lazy. By the time the browser gets around to fetching it, layout has happened, so it can measure the box instead of predicting it.

The auto goes first and the rest of the list is the fallback you were already writing: a browser that doesn’t understand that entry skips it and reads on, and one that gives up on the whole attribute lands back on 100vw, which is where you were standing anyway. So check support before you lean on it, but the worst case is the status quo, which is about as gentle as this stuff gets.

The hero can’t have it

Then there’s the catch, and it’s a real one. auto is only valid on an image with loading="lazy" on it, and that isn’t an arbitrary restriction, it’s the whole mechanism. An eager image gets fetched before layout exists. So there’s nothing to measure and nothing for the browser to do but guess the way it always has.

Which means the one image where the guess costs you the most, the hero, the eager one, the LCP, is the single image auto cannot help you with. I find that funny in a resigned sort of way. The platform got around to automating the half of the job where being wrong was cheapest, and left the expensive half exactly where it was.

So now I put auto on everything lazy, which is nearly every image on a page, and one hand-written sizes for the hero in the one template that renders it. That’s a single value per site to keep in step with the CSS instead of one per component, which is a genuine improvement and also quite a bit smaller than it sounded when I first read about it.

Still your problem

None of this touches the CMS. The build resizes what a developer commits, but the client uploads a 4000px photo through the form I built for uploading photos, and whether anything happens to that file depends entirely on whether somebody wired a transformation into the upload path, which is a decision about the CMS and not about HTML.

And none of it is the first thing to check anyway. If the source is the wrong shape for its slot, or came out of the export dialog at quality 100, then no attribute is going to save you. These decide which of your files goes down the wire. They have nothing to say about whether the files were any good to begin with.

The plain attributes won

Eight years on, the attribute I wrote a whole post about has been nerfed while nobody was looking. I’m not sorry about it. width, height, loading and fetchpriority between them are worth more on a typical page than every hour I’ve spent on breakpoint lists, and they’re all things you set once in a template and never think about again. sizes is the one that still wants your attention. It now wants it in one place per site instead of in every component that holds a picture.

So if you go and do one thing, don’t make it the sizes list. Put width and height on every image on the site, taken off the files themselves, then open the network panel at phone width with the cache off and look at what came down. That’s the order I’d have gotten backwards for most of the last eight years. I’d have been pretty confident about it, too.

Read similar posts
8 min

It was still encoding in the morning

I pointed an AVIF encoder at a client's product photography before bed and it was still grinding away at breakfast, which settled the format question for me faster than any benchmark did.

6 min

It got slower while I optimized it

I added lazy loading to every image on a site in about 20 minutes, felt pleased with myself, and then measured it and found the pages were taking longer to show their main content than before I started.