Blog
Goodbye legacy image formats. Hello modern image formats.
Back in 2015 when I made Imager, one of the key features was being able to optimize images generated with Imagick and GD, with tools and services like jpegoptim, jpegtran, optipng and TinyPNG (what I refer to as optimizers). Back then, Google Page Speed was all the rage and an essential factor to get a high score was managing to squeeze out the last couple of bytes from the jpeg and png formats. There were huge gains to be had for optimizing pngs, and equally tiny ones for optimizing jpegs. We spent countless hours installing, configuring and testing the various tools, and endless cpu cycles were being spent on servers all over the cloud. It all felt kinda pointless.
It's 2023, and I'll go out on a limb and say; stop wasting your (and your server's) time, forget about optimizers! 🔥 In fact, start treating legacy image formats like the outdated tech they are! 🔥🔥 Why?
Firstly, because the gains for using modern file formats like WebP and AVIF, far outweighs the gains you get from optimizing jpegs and pngs. So you should put time and effort into supporting those instead.
Secondly, with 96% global browser support for WebP, and around 80% for AVIF, it's time to start making "modern image format"-first websites. AVIF would be higher still if it weren't for Edge, which is the one holdout among the big four at this point. That's not a reason to wait, it's a reason to have a fallback chain.
And before anyone asks about JPEG XL; Chrome pulled support in 110 earlier this year, after two years behind a flag. It may well come back. Don't build your pipeline around it until it does. AVIF and WebP are what actually shipped.
Getting real
As developers, we tend to try to reach 100% whenever we optimize something. That is a noble cause, and sometimes we have to - and sometimes we want to. But the truth is that clients, more often than not, aren't willing to pay for 100%. And sometimes, reaching for those last 5% can seriously impact other aspects of a project.
What I'm about to propose will save you time, your server will have to do less processing and the end-result will be better for 95% of your users.
"Modern image formats"-first
The obvious first step when you go modern-first is to flip the whole thing on its head. Stop thinking about WebP and AVIF as a little progressive enhancement that you sprinkle on top of a lovingly crafted set of jpegs. The modern formats are the images you're delivering. The jpeg is what it should have been all along; a fallback.
And once you accept that, a lot of work just disappears.
In a traditional responsive setup you'd generate every breakpoint in every format, and then run every single one of those files through an optimizer. Five breakpoints in three formats is 15 transforms, 15 files on disk, and 15 optimizer runs. In a modern-first setup you generate the full srcset in AVIF and WebP, and then you generate exactly one jpeg. Not five. One.
{% set transforms = [
{ width: 600 },
{ width: 900 },
{ width: 1200 },
{ width: 1800 },
{ width: 2400 }
] %}
{% set avif = craft.imagerx.transformImage(image, transforms, { format: 'avif', quality: 65 }) %}
{% set webp = craft.imagerx.transformImage(image, transforms, { format: 'webp', quality: 70 }) %}
{% set fallback = craft.imagerx.transformImage(image, { width: 1400 }, { format: 'jpg', quality: 60 }) %}
<picture>
<source srcset="{{ craft.imagerx.srcset(avif) }}" sizes="100vw" type="image/avif">
<source srcset="{{ craft.imagerx.srcset(webp) }}" sizes="100vw" type="image/webp">
<img src="{{ fallback.url }}" width="{{ fallback.width }}" height="{{ fallback.height }}" alt="{{ image.title }}" loading="lazy">
</picture>
That's it. That's the pattern.
But the fallback will look bad on a 27" iMac!
Will it though? Have a think about who is actually going to be served that jpeg. It's Safari on an old iPad that hasn't seen an OS update since 2017, it's the Android stock browser on a phone someone found in a drawer, and it's whatever weird in-app webview a social platform shipped last week. It is, almost by definition, not a device with a huge retina display and a fibre connection.
So pick a size somewhere in the middle, around 1400px, at a quality you'd be comfortable with, and move on. The fallback doesn't need to be great, it needs to be fine. You are not going to lose the client over this.
And here's the thing people forget; the 5% getting the fallback used to get exactly that same jpeg anyway, back when jpeg was all we had. Nobody got hurt. Meanwhile the 95% are now getting files that are 30-50% smaller than the optimized jpeg you used to be so proud of.
You still need that one jpeg
At 96% support you might reasonably ask why the fallback is a jpeg at all, rather than just WebP. Here's why.
Social platforms still want a jpeg or a png for og:image. Email clients are, as always, living in the past. And every so often someone will right-click, save, and try to open the file in a piece of software from 2009. Having one sane, predictable jpeg per image solves all of that at once, and you were generating it anyway.
So the single fallback isn't a compromise you're grudgingly making for IE11. It's a useful file that happens to also cover the last 5%.
So what about the optimizers?
Turn them off.
'optimizers' => [],
Wait? Turn them OFF?!
I know. I built that feature, I like that feature, and I'm telling you to turn it off. Here's the math that made me change my mind:
- Running jpegoptim over an already lossy-encoded jpeg buys you a few percent, on a good day.
- Encoding that same image as AVIF at a comparable perceived quality buys you 30-50%.
You've been arguing over pocket change while there was a banknote on the table. And you were paying for that argument with server cpu, one transform at a time, forever.
The one place I'd still consider keeping an optimizer around is for the single jpeg fallback, if it makes you sleep better. It's one file, so the cost is negligible. But be honest with yourself about what you're getting for it.
Oh, and pngs. Just stop reaching for png for photographic or semi-photographic content. If you need lossless, WebP lossless will typically beat an optipng'd png anyway, so the optimizer story ends there too. Keep png around for what it's actually good at, and let WebP and AVIF do the rest.
Where the cpu cycles should go instead
Now, I'm not going to pretend this is free. AVIF encoding is still slow. It's improved a great deal over the last couple of years, and it's no longer the "did the request just die?" experience it was in 2021, but it's slow enough that you don't want it anywhere near the request cycle. That's a real cost and you need to plan for it.
But it's the right cost. It's a one-time cost per transform that gets cached and then serves a much smaller file to almost everyone, more or less forever. Compare that to an optimizer pass, which is also a one-time cost, but buys you almost nothing. Same budget, wildly different return.
Practically, this is what Imager X's auto generation is for. Rather than generating transforms on the first request, Imager can generate them in a queue job when the asset is uploaded or saved, so by the time anyone hits the page the AVIFs are already on disk. No first-visitor penalty, no cold cache, and you can afford to be greedy with encoding effort because nobody's sitting there waiting on it.The real shift is that transform generation stops being a page-load concern and becomes an asset-save concern, which is a much better place for it. Editors upload, the queue chews through it, the front-end just reads files.
Wrapping up
The web has moved on, and honestly the tooling and the habits haven't quite caught up. We spent years building elaborate optimization pipelines to chase a score, and somewhere along the way the actual win, better codecs, showed up and got treated as an optional extra.
So:
- Generate your srcsets in AVIF and WebP.
- Generate one jpeg fallback, at a sensible size, and use it for og:image too.
- Delete your optimizer config.
- Spend the cpu you just freed up on encoding, and do it in a queue.
Less setup, less server load, fewer files, smaller payloads for 95% of your users. That's not a trade-off, that's just a better default. 🔥