Just look at it
Dimensions before formats, and a compression quality nobody has ever once checked.
I got handed a marketing site last year that took about 11 seconds to become useful on a phone. The whole page was 6.5 MB, and 4.2 MB of that was one JPEG. A stock photo. Three people in a bright office looking delighted at a laptop.
There was also a folder in the repo called images-optimized, which I want to be gentle about, because somebody clearly cared. It contained the same images at JPEG quality 92. Which is optimization in the sense that it is a smaller number than 100.
I think images are the one performance problem where everybody already knows the diagnosis and almost nobody has done the thing. We all say “yeah, images are usually the biggest win.” And then we go tune a bundle.
The order matters more than the codec
Here’s the part I’d actually front-load, because it’s the least interesting and it’s where nearly all the bytes are.
Roughly: don’t ship the image, then ship it at the right dimensions, then in the right format, then at the right quality, then with the right loading strategy. In that order. Most of us start three steps in, at format, because format is the fun one. There’s a spec to read and a comparison chart and everything.
But that 4.2 MB hero was 5000 pixels wide and it was rendering into a container that maxed out around 1200. That’s the whole bug. Pixels are quadratic, so shipping something at four times the width you need isn’t four times too big, it’s 16 times too big, and no codec is going to save you from an image that’s four times too big. AVIF instead of JPEG might buy you 40%. Resizing it correctly buys you 90% before you’ve picked a format at all.
No codec is going to save you from an image that's four times too big.
And the step before that one is the awkward conversation, which is whether the image needs to exist. The delighted-laptop photo was meant nothing to anybody. It was there because the section looked empty without something in it. I don’t always win that argument and I don’t think I should always win it, visual weight is a real thing and I’m not a “just use system fonts and a wall of text” person. But it’s worth asking out loud once, because the cheapest image is the one you deleted.
So, look at it
Okay, here’s the actual recommendation, and it’s a tool rather than a rule: Squoosh. Drag an image in, and it gives you the original on one side, your compressed version on the other, and a slider you drag across the middle to compare them at the same spot, with the output size updating live as you turn the quality knob.
It runs entirely in your browser. The codecs are compiled to WebAssembly and nothing gets uploaded anywhere, which matters more than it sounds like it does, because it means you can put a client’s unreleased screenshots through it without thinking hard about it, and you can send the link to a designer or a marketing person without setting up an account for them.
The compression itself isn’t the point. If you have a build step, sharp will do this for you at a thousand images a minute and you should let it. The point is calibration. It teaches you what the numbers look like on your images, and you find out fast that the general advice can’t help you much, because a photo of foliage and a flat UI screenshot and a product shot on white behave like completely different media. Quality 75 is invisible on one of those and turns another into mush around the text.
Do the exercise once, seriously. Take the heaviest image on your site, drag the quality down until you can genuinely see it break in the slider, then come back up one notch. Whatever number you land on is your number, and I’d bet a small amount of money it’s lower than the one currently in your config. The compression isn’t the hard part. Deciding what you can live with is, and you cannot decide that from a blog post, including this one.
(Worth knowing: it’s the web app I’m recommending here, not a build step. This is a five-minute job you do once with your eyes open, not a pipeline.)
The formats, briefly and without a chart
AVIF is usually the smallest for photographs, sometimes dramatically. WebP is the boring safe one that’s supported literally everywhere and beats JPEG comfortably. JPEG is the fallback that will still be there when we’re all dead. Serve them with <picture>, best first, and the browser takes the first type it understands.
<picture>
<source type="image/avif" srcset="/img/hero.avif">
<source type="image/webp" srcset="/img/hero.webp">
<img class="hero__image" src="/img/hero.jpg" alt="…" width="1200" height="675">
</picture>
The thing the comparison charts undersell is that AVIF is not always the winner. On small images the header overhead eats the gains, and on flat graphics with hard edges it can lose to WebP outright, and it’s slow to encode, which you’ll feel if you’re doing it in CI on a few thousand files. This is exactly why the slider is worth more than the chart: you can find out in about eight seconds which one wins for the kind of image you actually publish, rather than which one won on somebody’s benchmark corpus.
JPEG XL is better than all of it and the browser support situation remains a slow-motion tragedy, so, someday. Anyway. And anything that’s a shape rather than a photograph, an icon, a logo, a chart, should be an SVG, run through SVGO, because what comes out of Figma is about 60% metadata and editor cruft.
The free stuff you’re probably not doing
Set width and height. Always, even when CSS is overriding both of them, because the browser derives the aspect ratio from those two numbers and reserves the space before the image arrives. That’s most of your layout shift, gone, for two attributes.
Lazy-load what’s below the fold with loading="lazy", and put fetchpriority="high" on the one image that’s your LCP, which on a marketing page is basically always the hero.
Then srcset and sizes, which is where the real responsive win is and also the one everybody gets wrong. srcset is easy, it’s a list of files and their widths. sizes is the hard one, because you’re telling the browser how wide the image will be before it has your CSS, and the default assumption if you say nothing is 100vw. So on a 44rem prose column at desktop width you’ve just told a 2560px monitor to go get the 2560px file for an image that renders at 700.
<img
class="post__figure"
src="/img/diagram-1200.webp"
srcset="/img/diagram-600.webp 600w,
/img/diagram-1200.webp 1200w,
/img/diagram-2000.webp 2000w"
sizes="(min-width: 64rem) 44rem, 100vw"
alt="…"
width="1200"
height="675"
loading="lazy"
decoding="async">
sizes="auto" exists now for lazy-loaded images, where the browser waits until it knows the real layout width and just uses that, which is obviously the correct design and I use it where I can. Support isn’t everywhere yet, so it degrades to 100vw, which is the status quo, so there’s not much downside to trying it.
“Just use an image CDN”
I know. And honestly, yes, do that. Next’s <Image>, eleventy-img, Cloudflare Images, imgix, whatever’s in front of you. They resize, they content-negotiate the format, they cache it, and they do it more consistently than a human maintaining a build script. If you’re starting something today I’d reach for one before I’d hand-roll any of the above, and I do.
Two things I’d still say, though.
First, the automation picks a quality number, and it’s someone else’s number, chosen conservatively because it has to be safe across every image anybody might ever upload. That’s the right call for a general-purpose service and it means you’re leaving something on the table on your particular content. Being able to open the thing and check its work is a useful skill, and it takes an afternoon to acquire, once.
Second, and more importantly, none of it can tell you the image shouldn’t be there, or write your sizes attribute, because sizes is a fact about your layout that only you know. The pipeline can make an image smaller. It can’t make it unnecessary.
Where I’d start
The reason I like Squoosh isn’t that it’s the most powerful tool in the category, it isn’t. It’s that it collapses the distance between “I changed a number in a config file” and “I have seen what that does.” Almost every bad image decision I’ve made, and I have made the 5000px one, came from never actually looking at the output 🫠
So maybe just go find the heaviest image on your own site tonight. Open the network tab, sort by size, take the top one, put it in the slider, and be a little embarrassed. It’s a very cheap 15 minutes and it’s the only part of this you can’t get from reading.
I work mostly on stores, where images are most of the payload and the LCP is either a product shot or the first row of a category grid, so this is the drum I have most reason to bang. If you’re shipping a data-heavy app where the images are 12 24px icons, go tune your bundle instead, none of this is about you. But if there’s a photo above the fold, I’d start there. It’s almost always still the biggest single thing you can fix.