Using WebP images with React 路 Josh W Comeau

In The 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 鈥攁nd 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鈥擨 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.

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:

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

The results are pretty dramatic:

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!

I haven't tested this on SVGs at all. SVG is a vector format, meaning that it's made up of mathematical instructions rather than individual pixel colors. It would be a shame to lose the scaling benefits of a vector format, and I suspect it would actually increase the file size in most cases.

.webp enjoys support in most browsers:

Critically, we're missing Safari and Internet Explorer.

How about JPEG 2000?

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

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:

<picture><source srcset="/images/cereal-box.webp" type="image/webp" /><source srcset="/images/cereal-box.jp2" type="image/jp2" /><img src="/images/cereal-box.jxr" type="image/vnd.ms-photo" /></picture>

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:

<img src="/images/cereal-box.webp" />

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 picture element is too modern a feature for Internet Explorer, and yet this code snippet still works as intended 馃槷This is because when the browser hits an element it doesn't understand, it treats it as a div. So what we wind up with is a bunch of divs, and an <img> tag that points at the jxr image, which just so happens to be the format that Internet Explorer understands!

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:

<source srcset="/images/cereal-box.webp" /><img src="/images/cereal-box.jpg" />

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 cwebp, 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 package from their repository.

once installed, you can use it like this:

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:

const ImgWithFallback = ({  type = 'image/webp',}) => {return (<picture><source srcSet={src} type={type} /><img src={fallback} {...delegated} /></picture>);

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:

The reason this works is because of how the styled helper operates. It generates a class and injects it into the document's stylesheet, and then passes the generated class name down as a prop:<ImgWithFallback className="sc-some-generated-thing" />We're delegating all properties to the child img tag, so the right styles still make it to the image. Joyfully, everything works like you'd expect.

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.

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 =)

Unsurprisingly, an article on image formats doesn't tend to excite developers, but I think it's really valuable! It's probably the easiest way to shave hundreds of kilobytes off your webapp / website.

If you're active on Twitter, I'd really appreciate a share!