Embracing modern image formats


In The Cost of JavascriptThe Cost of Javascript, Addy makes a really good point: 200kb of Javascript is more "expensive" than 200kb of images, because the browser needs to do more work to use code compared to images. From the article:

A JPEG image needs to be decoded, rasterized, and painted on the screen. A JavaScript bundle needs to be downloaded and then parsed, compiled, executed —and there are a number of other steps that an engine needs to complete. Just be aware that these costs are not quite equivalent.

This is still very true, but it's a little less significant at this exact moment in history.

With a pandemic sweeping across the globe, I've found that my internet has gotten pretty choppy. Fortunately, because Site Reliability Engineers are both brilliant and tireless, most of the internet is still up and running, but there's definitely something going on—I have a 100mbps connection, but it feels more like 3G at the moment.

This shifts the calculation a little bit. Our devices can still parse and compile javascript at the same speed they could a couple weeks back, but network speeds have gotten slower. So the raw number of bits over the wire is super important right now!

And sites typically have way more than 200kb worth of images; it's not uncommon for a page to have several megabytes of images. Many developers (myself included!) tend not to think about media size much at all.

Happily, there's some pretty low-hanging fruit! In this tutorial, we'll see how we can leverage "next-gen" image formats like WebP. These images are often 2-3x smaller than the legacy formats we know and love (jpg, png). It can make a huge difference.

Prefer your lessons in video format? Watch for free on egghead:

There are three formats that we can use:

  • JPEG 2000 — an iterative improvement on jpgs. Developed in 1997 primarily for use in film and medical imaging. Allows images to be compressed further, with less artifacts.
  • JPEG XR — A cousin of jpeg2000, developed by Microsoft in 2009
  • Webp — a format developed for the web by Google in 2010, focused on using advanced optimization techniques to reduce file-size. Supports transparency and even animation.

We'll spend most of our time today talking about webp, but we'll revisit the jpeg cousins when we discuss browser compatibility.

A few months ago, I used this image in a post:

Photo of cereal expiration dates

I did some experiments, using both jpg and png for the source image. I optimized them using imageminimagemin, to see how good these "retro" formats could get.

The results are pretty dramatic:

DetailsOriginalConverted
(from Photoshop)742kb61kb!92% smaller
(using imagemin)178kb58kb!67% smaller
(from Photoshop)242kb50kb!79% smaller
(using imagemin)151kb50kb!67% smaller

I've tested it on a lot of images, and it almost always produces files that are 30-70% smaller than even the optimized images!

.webp enjoys support in most browsers:

Browser support table for webp, showing support in every browser except Internet Exporer, Safari, or KaiOS browser

Critically, we're missing Safari and Internet Explorer.

How about JPEG 2000?

Browser support table for jpeg2000, showing support in Safari and Mobile Safari, but nothing else

Alright, so we've filled in Safari, but there's still that pesky Internet Explorer…

Browser support table for jpegxr, showing support for Internet Explorer, and Internet Explorer alone

We've hit caniuse bingo! With these 3 image formats, we have perfect coverage across the browser spectrum.

Let's look at how we pick and choose different formats for different browsers

HTML has two image media elements: the international pop-star img, and the niche hipster artist picture.

picture is a much newer addition to the language. Its main goal is to let us load different sources depending on resolution or support for a given image format.

Here's what it looks like:

The picture tag supports a bunch of source children. The browser parses the source elements in sequence, looking for the first one it can use based on the type. When it finds one, it works out where the image lives via srcset, and swaps it into the img's src

srcset can do a lot of complicated things, but happily for our usecase, we can treat it the same as src. Essentially, source is config, and it plugs the matching value into the img.

In Chrome, for example, we wind up with something more-or-less equivalent to this:

This cascade of sources means that one will match on every browser: Most browsers will use webp, Safari will use jp2, and IE will use jxr.

The snippet above excels in its ability to match every possible browser with a modern "next-gen" image format. But it assumes that these images exist in these formats.

If we're creating these images by hand, it's a lot of manual labor. And if we're generating them automatically, it can significantly lengthen our build time; image processing is notoriously slow when done at scale.

On my own blog, which receives very little Internet Explorer traffic, I've opted for a lazier solution:

I'm serving the nice and tiny webp to browsers that support it (Chrome, Firefox, Edge), and falling back to a legacy jpg for browsers that don't (IE, Safari).

To me, this is an example of progressive enhancement. Everything still works on legacy browsers, but images will be a bit slower to load. This is a trade-off I am alright with.

(Hopefully Apple will get on this train soon though! 🤞🏻)

The browser devtools will always think that the image has whatever src you gave it initially. If you inspect it in the elements pane, you'll see that it uses a .jpg.

To check if it's actually working, the best trick I've found is to right-click and "Save as…". On Chrome, you should get a "Google WebP" file format, whereas on Safari or IE you should get a "JPEG".

You can also check the network tab, to see which was actually downloaded.

Google has created a suite of tools to help us work with webp files. One of those tools is cwebpcwebp, which lets us convert other image formats to webp.

If you're on MacOS, you can install the suite with Homebrew:

On other platforms, I believe you'll need to download the appropriate libwebp packagelibwebp package from their repository.

once installed, you can use it like this:

  • -q 80 is a flag to set the "quality", from 1 (worst) to 100 (best). You can experiment with different values. I've found that 70-80 is the sweet spot.
  • cereal.png is the path to the input file you want to convert.
  • -o cereal.webp is output path.

A component is a brilliant way to abstract over some of the funkiness with the <picture> element. Here's what I've been using, to glorious effect:

We can use ImgWithFallback very similarly to how we'd use an img tag:

If you use styled-components or Emotion, you may be used to wrapping images in a styled wrapper:

Thankfully, this still works with our ImgWithFallback component. We can wrap it like any other component:

If you're developing with Gatsby, the gatsby-image package already does a bunch of optimizations out of the box, including converting to webp (though you need to opt in for it).

Gatsby Image isn't meant as a drop-in replacement for img; it can be a bit more friction to use, but it also comes with a lot of additional magic tricks for your trouble.

Check out the docsthe docs for more info.

The only real downside I've found so far is that webp is an annoying format to work with as a user.

Most desktop software doesn't yet support it; I can't open it in Preview on MacOS, for example. This means if I right-click and "Save as…" a webp image, I won't be able to view it!

Converting a webp to a jpg is relatively painless, and a google search turns up many online providers that will do it for free. But still, it's an additional bit of friction. If your site/app encourages users to download images, you might not want to make this switch.

I'm pretty happy to have cut the size of images on my blog by ~50%. In addition to the benefits to user experience at a critical time, I'm also expecting that this'll save me some money in terms of bandwidth.

Of course, it doesn't seem practical to manually convert every image I use to webp. I'm already investigating how I can generate these images automatically from the source jpg and png files. Ideally, this isn't something I should ever have to think about, it should happen automatically when I build my site. Expect to see something on that soon =)

3D portrait of the blog's author, Josh Comeau

My goal this year is to create lots of rich, bite-sized tutorials for React developers. If you liked this one, you'll love what I'm working on! Readers receive ✨ early access ✨ to new content. There will be no spam and you can unsubscribe at any time.