Using WebP with Modernizr

WebP is a new image format from the clever people over at Google. The lossy variant is 25-34% smaller than JPEG, while the lossless variant is typically 26% smaller than PNG. It's already supported in Chrome and Opera (both desktop and mobile), and the native Android browser.

There's a strong argument for detecting WebP support on the server – see this article for a full discussion. But if you choose to implement it client-side, read on.

Modernizr is able to detect support for WebP, but the test is asynchronous which makes things a bit tricky. Modernizr v3 adds the Modernizr.on() method to help with exactly this – and there's a polyfill for earlier versions so you can use it right now.

I thought I'd give a few pointers on how best to go about this.

These examples assume you're using a Modernizr build including the webp test and the cssclasses build option (like this) in your document <head>.

Asynchronous test recap

I said the WebP test is asynchronous. What exactly does that mean?

Simply put, we don't get a result for the test until a little while after the rest of Modernizr runs. It's likely that the browser will have gone on to load the rest of the page — and may even have finished — by the time we get a result.

In reality it's really quick and usually finishes within a few milliseconds, but it might take longer so it would be dilligent to plan for the worst case.

Async Modernizr classes

If you've included the cssclasses build option, Modernizr will add a class named either webp or no-webp to the <html> element on your page to reflect the result of the test. Again, this will happen after a short delay and until then Modernizr will not add a class for WebP:

<html class="no-js">

<!-- Modernizr runs and changes `no-js` class to `js` -->

<html class="js">

<!-- WebP test completes -->

<html class="js webp">

We'll look at the effect of this shortly.

Async Modernizr booleans

Until the test has completed, Modernizr.webp will be undefined:

<!-- Modernizr build including the `webp` detect -->
<script src="modernizr.custom.js"></script>
<script>
  console.log(Modernizr.webp); // undefined
</script>

But with Modernizr.on() you can get it to let you know when the test completes:

Modernizr.on('webp', function (result) {
  // `result == Modernizr.webp`
  console.log(result);  // either `true` or `false`
  if (result) {
    // Has WebP support
  }
  else {
    // No WebP support
  }
});

The callback will fire either when the tests completes, or almost immediately (on next tick) if it has already completed when Modernizr.on() is called.

In CSS

Let's start with using WebP for images in stylesheets. The obvious use case is a background image for some container:

<div class="container"></div>

No fallback

It's worth considering whether you need a fallback. If it's a background image, is it really that important? If you decide the answer's "no", the solution here is easy:

.webp .container {
  background-image: url('image.webp');
}

That's it. Once Modernizr has determined that WebP is supported, this rule will match and the browser load the image. If WebP isn't supported, or if JavaScript isn't available, this rule will never match so no requests are wasted.

With fallback

If the fallback is important, we can add one easily.

.webp .container {
  background-image: url('image.webp');
}
.no-webp .container {
  background-image: url('image.jpg');
}

The whole point of WebP is the reduced file size, so it's important we only perform one request: either for the WebP or the fallback, not both. The .no-webp selector on the fallback rule ensures this, by stopping it from matching until the result has returned; otherwise we'll end up with 2 requests.

If JS isn't supported, no Modernizr classes will be added, so neither rule will match and no image will be shown. If providing an image for non-JS users is important to you, read on...

With non-JS fallback

It gets slightly more complicated now. We have 4 cases/states we have to account for:

  • No JS: use fallback
  • JS + detect result pending: no image
  • JS + not supported: use fallback
  • JS + supported: use WebP

This pattern covers all of these:

/* Result pending */
.js .container {
  background-image: none;
}
/* No JS / WebP not supported */
.no-js .container,
.js.no-webp .container {
  background-image: url('image.jpg');
}
/* WebP supported */
.js.webp .container {
  background-image: url('image.webp');
}

Adding .js to all the JS cases is important, even on the .webp and .no-webp rules, to ensure the correct precedences.

In <img> tags

Let's move on to using WebP in <img> tags. Because these are typically content images (and hence important), I'm going to assume a fallback is always required.

Don't forget to provide an explicit download link for any images users might want to save to their device.

Again, we need to make sure the browser only downloads one file: a WebP or a fallback image; not both. Browsers fetch any images linked in <img> elements without thinking twice:

<img src="image.webp" alt="My image">
<!--
  Too late: image.webp has already been requested, which will waste bandwidth
  for browsers which can't render it.
-->

So instead, we can omit the src attribute, storing the possible paths in data- attributes:

<img data-jpg="image.jpg" data-webp="image.webp" id="myimg" alt="My image">

...and add it in once we know which image to load:

// Set `src` attribute based on the result of Modernizr.webp
Modernizr.on('webp', function (result) {
  var img = document.getElementById('myimg');
  if (result) {
    img.src = img.getAttribute('data-webp');
  }
  else {
    img.src = img.getAttribute('data-jpg');
  }
});

Adding the src attribute with JavaScript means the image won't show if JavaScript isn't enabled. If this matters to you, read on...

Fallback without JavaScript

There are various ways to provide a fallback when JavaScript isn't available. I'm going to show you a way which does it fairly cleanly, accessibly and safely. There's a little bit of repetition, but I'm OK with that.

<noscript data-jpg="image.jpg" data-webp="image.webp" data-alt="My image"
          id="myimg">
  <img src="image.jpg" alt="My image">
</noscript>
Modernizr.on('webp', function (result) {
    // Copy attributes from the `<noscript>` to a new `<img>` element
    var nscript = document.getElementById('myimg');
    var img = document.createElement('img');
    img.alt = nscript.getAttribute('data-alt');

    // Set the `src` based on whether WebP is supported
    if (result) {
      img.src = nscript.getAttribute('data-webp');
    }
    else {
      img.src = nscript.getAttribute('data-jpg');
    }

    // Inject into the DOM in the correct place (after the `<noscript>`)
    nscript.parentNode.insertBefore(img, nscript.nextSibling);
});

Here's what's going on:

  • The image is in a <noscript> tag with the src sttribute pointing to the JPEG, so non-JS users will see the fallback
  • This will be hidden from JS users, so we put any attributes we'd like to be on our <img> tag on the parent <noscript> tag
  • We create a new <img> tag and copy attributes over from the <noscript>
  • As before, if WebP support is detected, we set the src attribute to point to the WebP file; otherwise the JPEG
  • The <img> tag is injected into the page where it would have appeared in normal flow

Lossless WebP

WebP also has a lossless mode, providing a direct alternative to PNG. Modernizr can detect support for this separately with the Modernizr.webplossless property (if you include this in your build). This can be used in exactly the same ways as I've just described for (lossy) webp.