Webp-ing your site: reduce image file size, increase site performance
Tags: javascript, html, vue, node
Published: 2019-04-06 02:11:45

## TL;DR

The `.webp` image format can offer drastic improvements in image file size... I'm talking up to 90% reduction in size (from their `.jpg` or `.png` counterparts). Aside from Safari and IE, `.webp` images have broad browser support, and with tools like [sharp](http://sharp.pixelplumbing.com/en/stable/), you can easily take your collection of `.jpg`s and `.png`s and convert them to `.webp` images to serve to browsers that can handle them. It's relatively straight-forward to detect browsers that don't support `.webp`, and I'll show you one way to handle those browsers.

The code for this example can be found [on GitHub](https://github.com/gksander/webp-vue). 

## Back story

I'm a software engineer that works in the graphics/printing industry, so I deal with images... a lot. Most applications I build involve a lot of image (users uploading and editing them, and displaying large collections of user-uploaded content). It's in our best interest to optimize images for the web when displaying them back to users (reduced bandwidth), but more importantly - it can drastically improve site performance and page load speed (since significantly less data is being sent to the user). Recently, I transitioned to using `.webp` whenever possible, and I'll show you some of the tricks I picked up.

## Want to follow along?

If you want to follow along, check out the code [on GitHub](https://github.com/gksander/webp-vue). You can clone the repository and then run an `npm install` from the root of the folder (there's only one node module needed, but it's an important one).

## How do I create `.webp` images?

**[sharp](http://sharp.pixelplumbing.com/en/stable/). That's how.**

There's a decent chance that nobody has ever emailed you a `.webp` image file, or you've never opened one with a graphics application. So is it a total pain in the arse to transition into using `.webp` images in your site? At first, I thought so. Turns out, it's not that bad.

Since I regularly deal with large user-uploaded image files, I'm used to converting images into web-ready versions. My back-end technology of choice is Node, and luckily there's an amazing node library for processing images: [sharp](http://sharp.pixelplumbing.com/en/stable/). Sharp is **insanely** fast at manipulating images. You can resize and crop a photo in milliseconds. It's a life-saver for me.

You can feed sharp a variety of image types (`.jpg`, `.png`, `.tiff`, etc.), manipulate the image, and output it in various formats - including `.webp`. I generally use sharp to web-ify user-uploaded images, but it's equally as useful in writing scripts to process your own files. I'll show you one way to use sharp to make `.webp` copies of your site's images.

If you have my sample repository opened, you can see there's a folder titled `/images` that contains some images. The only "original" images are `dog.jpg` and `chair.png`. We're going to generate the other files.

There's also a file called [process-images.js](https://github.com/gksander/webp-vue/blob/master/process-images.js) that has the following content:

```js
// Import sharp (processing images) and path (traversing directory)
const sharp = require('sharp');
const path = require('path');

// Create an asynchronous IIFE
(async function(){
  // Where are our image files located?
  const imageDirectory = path.join(__dirname, './images');
  // Which images should we process?
  const imageNames = ["dog.jpg", "chair.png"];
  // What size should we crop to?
  const maxSize = 1000;

  // Loop through the images and process them one at a time.
  for (let imageName of imageNames) {
    try {
      // Start by creating a jpg version
      await sharp(path.join(imageDirectory, imageName)) // This inputs the file into sharp
        .resize(maxSize, maxSize, { fit: "inside" }) // This resizes our image
        .toFile(
          path.join(imageDirectory, imageName.replace(/\.(jpg|png)$/, `_${maxSize}$&`)) // Replace file extensions with .jpg (assumes .jpg or .png)
        ); // This writes the new image.

      // Same thing, but create a .webp version
      await sharp(path.join(imageDirectory, imageName))
        .resize(maxSize, maxSize, { fit: "inside" })
        .toFile(
          path.join(imageDirectory, imageName.replace(/\.(jpg|png)$/, `_${maxSize}.webp`)) // Replace file extensions with .webp (assumes .jpg or .png)
        ); // This writes the new image.

    } catch (_) {}
  } // End loop

  process.exit();
})();
```

This is the script that will take our "original" image files and create `.webp` versions of them. Here's what's going on in this file:

We import the `path` and `sharp` node modules (`path` is a native one). Then, we're going to run a function to process our files. At the top of that function you'll see:

```js
// Where are our image files located?
const imageDirectory = path.join(__dirname, './images');
// Which images should we process?
const imageNames = ["dog.jpg", "chair.png"];
// What size should we crop to?
const maxSize = 1000;
```

This sets some values to use, such as where the files are stored (the `imageDirectory` variable), what image files to process (the `imageNames` array), and what size to crop them to (`maxSize`). Then, we'll loop through each of the files in `imageNames` and process them.

We'll start by just resizing the "original" image:

```js
await sharp(path.join(imageDirectory, imageName)) // This inputs the file into sharp
  .resize(maxSize, maxSize, { fit: "inside" }) // This resizes our image
  .toFile(
    path.join(imageDirectory, imageName.replace(/\.(jpg|png)$/, `_${maxSize}$&`))
  ); // This writes the new image.
```

This feeds the image into sharp, tells sharp to resize it, and then outputs the file. The scary expression

```js
imageName.replace(/\.(jpg|png)$/, `_${maxSize}$&`)
```

Just tells sharp to add a "_1000" before the file extension, so `dog.jpg` will become `dog_1000.jpg` and `chair.png` will become `chair_1000.png`.

We'll run a similar process, but add a `.webp` extension to the file. Sharp will automatically write that file as a `.webp` file - *which is where the magic happens*. For each of our "original" files, we should have a cropped variant, as well as a cropped `.webp` variant - all in the same folder. 

Once we've got the script written, we need to run the following command from a command line:

```bash
node process-images.js
```

That's all it takes to process our files! As a fun extension, you could easily extend that script to create multiple different sizes of each image (say, one for a thumbnail and one for a "hero" shot).


## Using our new images

Most browsers support `.webp` images - but Safari and IE do not. I think Safari has a large enough browser market-share to justify having a fallback for browsers that don't support `.webp` images (I try to pretend like IE doesn't exists anymore, but this should handle IE as well). For illustrative purposes, I'm going to display a simple Vue "app" that will show `.webp` images when possible, and fall back to `.jpg` or `.png` when necessary.

In the code repo, you'll find a file [index.html](https://github.com/gksander/webp-vue/blob/master/index.html) which contains *very bare* HTML and a sprinkle of Vue to show how you could sprinkle in the `.webp` images. The `index.html` file contains a tiny bit of HTML:

```html
<div id="app">
  <h1>Webp supported: {{ webpSupported ? 'Yes' : 'No' }}</h1>

  <!-- Show the chair photo -->
  <img
    :src="transformImgExt('/images/chair_1000.png')"
    width="150px"
  />

  <!-- Show the dog photo -->
  <img
    :src="transformImgExt('/images/dog_1000.jpg')"
    width="150px"
  />

</div>
```

The `img` tags is where we'll display our new images. If you're not familiar with Vue.js, the `:src` attribute of the image tags indicates to Vue that we want to have a dynamic `src` attribute with the value given. We're going to write a function `transformImgExt` that will take an image URL and replace it with a `.webp` version if appropriate. So for example, `transformImgExt('/images/chair_1000.png')` will give us the relative url for `/images/chair_1000.png`, but try to replace it with `/images/chair_1000.webp` if the browser supports `.webp` images.

## Detecting browser support

Let's dig into the JavaScript we'll need to detect support for `.webp`. Here's the JS in the `index.html` file. (If you aren't familiar with Vue.js, don't worry too much about the details.)

```js
let app = new Vue({
  // What should we mount our Vue instance to?
  el: "#app",

  // App data
  data: {
    // We'll initially assume webp is supported
    webpSupported: true
  },

  // Methods
  methods: {
    /**
     * Helper to transform image extension.
     * Checks if webp is supported, and will swap out the image extension accordingly.
     */
    transformImgExt (url) {
      // If webp is supported, transform the url
      if (this.webpSupported) {
        return url.replace(/\.\w{1,5}$/, ".webp");
      } else { // Otherwise, just return the original
        return url;
      }
    }
  },

  /**
   * When app is "created", we'll run some checks to see if the browser supports webp
   */
  created() {
    (async () => {
      // If browser doesn't have createImageBitmap, we can't use webp.
      if (!self.createImageBitmap) {
        this.webpSupported = false;
        return;
      }
      
      // Base64 representation of a white point image
      const webpData = 'data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoCAAEAAQAcJaQAA3AA/v3AgAA=';
      // Retrieve the Image in Blob Format
      const blob = await fetch(webpData).then(r => r.blob());
      // If the createImageBitmap method succeeds, return true, otherwise false
      this.webpSupported = await createImageBitmap(blob).then(() => true, () => false);

    })();
  } // End created

})
```

You'll see a `data` property in the code:

```js
// App data
data: {
  // We'll initially assume webp is supported
  webpSupported: true
}
```

This is our "application" state. We'll create a state property called `webpSupported` to hold a boolean indicating whether or not we've got support for `.webp`. We'll potentially change this value once we do some "sniffing" to see if our browser can handle the `.webp` images.

Next, let's jump down to the `created()` section:

```js
/**
 * When app is "created", we'll run some checks to see if the browser supports webp
 */
created() {
  (async () => {
    // If browser doesn't have createImageBitmap, we can't use webp.
    if (!self.createImageBitmap) {
      this.webpSupported = false;
      return;
    }
    
    // Base64 representation of a white point image
    const webpData = 'data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoCAAEAAQAcJaQAA3AA/v3AgAA=';
    // Retrieve the Image in Blob Format
    const blob = await fetch(webpData).then(r => r.blob());
    // If the createImageBitmap method succeeds, return true, otherwise false
    this.webpSupported = await createImageBitmap(blob).then(() => true, () => false);

  })();
} // End created
```

This is using a technique I found in [this article](https://ourcodeworld.com/articles/read/630/how-to-detect-if-the-webp-image-format-is-supported-in-the-browser-with-javascript). This snippet checks to see if the browser has a `createImageBitmap` method - if not, `.webp` is not supported. Then, we'll create a base64 encoded webp image as a blob and try to create an image bitmap from it. If we can, then the browser supports `.webp`. There are some technical details behind that, but that's beyond the scope of this post.

At the end of the snippet, you'll notice the statement: 

```js
this.webpSupported = await createImageBitmap(blob).then(() => true, () => false);
```

The RHS of the statement tries to create the image bitmap, and if it's successful the `() => true` function expression will run (returning `true`), otherwise the `() => false` function expression will run (returning `false`). This gives us a value for `this.webpSupported`, which references that application state property we looked at earlier. At this point, our `data.webpSupported` property holds a boolean that actually tells us whether or not our browser supports `.webp` images.

We've got one last thing to look at: the `transformImgExt` method:

```js
/**
 * Helper to transform image extension.
 * Checks if webp is supported, and will swap out the image extension accordingly.
 */
transformImgExt (url) {
  // If webp is supported, transform the url
  if (this.webpSupported) {
    return url.replace(/\.\w{1,5}$/, ".webp");
  } else { // Otherwise, just return the original
    return url;
  }
}
```

This method will take a url, and if `.webp` is supported it'll swap out the file extension for `.webp`. Otherwise, it'll just give you the url back.

Let's inspect the line

```js
return url.replace(/\.\w{1,5}$/, ".webp");
```

a bit further though. If you're not familiar with Regular Expressions in JS, this probably looks like random characters. We're using the string "replace" method. The `/\.\w{1,5}$/` is a regular expression that looks for filename extensions. The `\.` at the start indicates a ".", the `\w{1,5}` will look for 1 to 5 letters (word characters?), and the `$` at the end indicates that it should be at the very end of the string. If we find something that matches, we'll replace what we found with ".webp". This should replace ".jpg" or ".png" with ".webp". (Be careful! This will also transform ".pdf" to ".webp". You could tighten this down more, if needed.)

Now, we can use the `transformImgExt` to try to serve up a `.webp` image extension if our browser can handle it. We saw this earlier.

## See it in action

I put these resources in a CodePen to showcase this. (The image url's are different because they were uploaded through CodePen.) If you open the pen in Chrome, FireFox, or Edge you should see that the images are indeed `.webp`. Right click on one and inspect it, or open the image in a new tab and notice that it is indeed a `.webp` image. If you open the pen in Safari or IE, you should see that the images are `.jpg` or `.png` images instead.

{% codepen https://codepen.io/gksander/pen/QPEMeZ %}

## How much did we save?

The way `.webp` files are compressed is pretty neat, but your benefit will vary from image to image. Let's look at the savings from this example.

* `dog_1000.jpg` has a size of 122 KB. `dog_1000.webp` has a size of 90 KB. That's a 25% savings. *Not bad!*
* `chair_1000.png` has a size of 778 KB. `chair_1000.webp` has a size of `81KB`. **That's an 89.5% savings**. That is amazing.

In real-world use, I'm getting on average somewhere between 40-60% savings. `.png` images seem to provide the most savings, and `.webp` images support transparency!

## Closing comments

`.webp` is neat. There are some potentially huge file-size savings for using them. However, it takes a little work to get them integrated into a site. Here are some miscellaneous closing comments relative to this.

* With sharp, you can control various aspects of the "to-webp" conversion, such as whether you want the compression to be lossless or not, and so on. 
* Sharp is *extremely* fast, so it's inexpensive to do these types of conversions.
* I've been "webp-ing" static site assets as shown above, but also "webp-ing" user uploads. Generally, when a user uploads a file I'll create a resized `.jpg` version, as well as a resized `.webp`. Sharp tears through these in a lightning-fast fashion.
* I generally create a method such as the `transformImgExt` shown above, but expose it throughout my app and use it wherever I'd like to display `.webp` images. This makes it reusable, and isn't that much work to factor the new image format into your app (with fallback support for Safari and IE).