A Fast & Responsive Background Image

While attempting to have a full-screen background image on my homepage without sacrificing performance, I decided to use media queries to deliver images suited to the device width, as well as implementing a responsive version of the “Blur Up” technique for loading background images. Since this turned out to be a little tricky, I figured I would write up a tutorial on how I did it. You will probably want to read the original article, and reference it as you go along if you want to follow along.

The “Blur Up” Technique

Originally attributed to Facebook, the technique was developed specifically for large background images:

The (ingenious) solution was to return a tiny image (around 40 pixels wide) and then scale that tiny image up whilst applying a gaussian blur. This instantly shows a background that looks aesthetically pleasing, and gives a preview of how the cover image would look. The actual cover image could then be loaded in the background in good time, and smoothly switched in. Smart thinking!

To see a working example, check out the finished CodePen:

See the Pen VazbLL by Jacob Smith (@iakobos) on CodePen.

Getting Started

The first step is create all the images for our breakpoints. I’ve taken a very large image from Unsplash and created images using the Bootstrap media query sizes as a guide:

Image name Screen size
bg-768.jpg < 768px
bg-768@2x.jpg < 768px Retina
bg-992.jpg < 992px
bg-992@2x.jpg < 992px Retina
bg-1200.jpg < 1200px
bg-1200@2x.jpg < 1200px Retina
bg-2560.jpg < 2560px
bg-full.jpg > 2560px

And I’ve also created an image named bg-40.jpg that will serve as the blurred placeholder on page load. I’ve ran the images through ImageOptim with lossless compression to make them smaller, and GitHub pages uses GZIP, so they’ll be delivered pretty quickly. You can find the images here.

Creating the svg

The next step is to create an SVG from the 40 pixel JPEG so that the blur filter can be applied (see the source article for an explanation). I’m on a Mac, so I used the openssl command to convert the JPEG to base64 and copy it to my clipboard:

$ openssl base64 -in app/images/bg-40.jpg | pbcopy

And then created the file app/images/bg-blur.svg, using the base64 I just copied, and the dimensions of my full image (2560x1511):

<svg xmlns="http://www.w3.org/2000/svg"
    xmlns:xlink="http://www.w3.org/1999/xlink"
    width="2560" height="1511"
    viewBox="0 0 2560 1511">
  <filter id="blur" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
    <feGaussianBlur stdDeviation="20 20" edgeMode="duplicate" />
    <feComponentTransfer>
      <feFuncA type="discrete" tableValues="1 1" />
    </feComponentTransfer>
  </filter>
  <image filter="url(#blur)"
        xlink:href="data:image/jpeg;base64,/9j/4AA ...[truncated]..." x="0" y="0"
        height="100%" width="100%"/>
</svg>

Inlining the critical CSS

Next, I added the critical CSS inline (you can’t see this on CodePen since we can’t modify the <head>, but you can see it on the finished website).

<head>
  <!-- ... -->
  <style>
    html {
      height: 100%;
      background-color: rgba(27, 43, 58, 1);
      background-image: url('data:image/svg+xml;charset=utf-8,%3Csvg...[truncated].../svg%3E');
      background-position: 50% 0;
      background-size: cover;
    }
  </style>
</head>

To get the background color, I used Sip to grab a dominant color from the original image. To generate the URI encoded SVG, I used the SVG Encoder that the original article linked to. I also set a 100% height, since I’m using the document element and not a header image.

If you’re following along, you should now see the blurred background image.

Responsive background images

To deliver the smallest necessary image1, I used a mobile-first approach by using the smallest size as the default and applying larger background images inside media queries. Since all of the background-image properties are in a single file, it should only load the background image that applies to the viewport size (and it does in my testing), but I’m not 100% sure about how different browsers handle image fetching inside media queries. I won’t write out the CSS here, but the idea is that each screen size has an associated background image, as well as a keyframe animation that animates the blur (though I think this only works on WebKit). You can check out the CodePen for the detailed CSS.

Adding the CSS isn’t enough to get the full image to load, though, we need to add some JavaScript to add the class after the image is loaded. I had to change the original pretty drastically to use the computed style instead of the static style sheet to accommodate the media queries.

// Inspired by https://css-tricks.com/the-blur-up-technique-for-loading-background-images/
window.onload = function loadStuff() {
  var bgStyle, bgSrc, bgLoader, img, el, enhancedClass;

  // Quit early if older browser (e.g. IE 8).
  if (!('addEventListener' in window) || !('getComputedStyle' in window)) {
    return;
  }

  el = document.documentElement;
  enhancedClass = 'is-enhanced';

  // this will be a zero-height element to load in the image
  bgLoader = document.createElement('div');
  bgLoader.classList.add(enhancedClass);
  // most browsers won't load the image unless it's actually in the DOM
  document.body.appendChild(bgLoader);
  // Get the image url based on which media query is applying
  bgStyle = getComputedStyle(bgLoader)['background-image'].match(/url\(['"](.+)['"]\)/);
  bgSrc = bgStyle && bgStyle[1];

  img = new Image();

  // instead of just using onload (which fails to fire from time to time),
  // add the src immediately and check for whether the image download is
  // complete before adding the onload handler
  if(bgSrc) {
    img.src = bgSrc;
  }

  function enhance() {
    bgLoader.remove();
    el.classList.add(enhancedClass);
  }

  if(img.complete || img.readyState === 'complete') {
    enhance();
  } else {
    img.onload = enhance;
  }
}

Conclusion

At this point, the image is loading extremely quickly, and only loading an appropriately sized image based on the screen size. It’s progressively enhanced (it will show only the blurred image if JavaScript doesn’t load), and the blur is animated on WebKit browsers.

  1. Technically it would be a little bit more efficient to subjectively judge at which point the image reached an unacceptable level of pixelation, but this seemed like a little bit too much work. I may eventually implement this using the method outlined here.