The “Blur Up” Technique for Loading Background Images

Avatar of Emil Björklund
Emil Björklund on (Updated on )

📣 Freelancers, Developers, and Part-Time Agency Owners: Kickstart Your Own Digital Agency with UACADEMY Launch by UGURUS 📣

The following is a guest post by Emil Björklund. Filter effects in CSS have been around for a while, and together with things like blend modes, they bring new possibilities for recreating and manipulating stuff in the browser that we previously had to do in Photoshop. Here, Emil explores a performance technique using one of the more forgotten filter effects – the filter function – as well as recreating it with SVG.

This all starts with a post from the Facebook engineering team on how they load cover photo previews in their native apps. The problem they faced is that these “cover photos” are large and often take a while to load, leaving the user with a less-than-ideal experience when the background suddenly changes from a solid color to an image.

This is especially true on low-connectivity or mobile networks, which often leave you staring at an empty gray box as you wait for images to download.

Ideally the image would be encoded into the initial API response of their app when getting the profile data. But to fit inside this request, the image would have to be capped at 200 bytes. Troublesome, as cover photos are over 100 kilobytes in size.

So how do you get something valuable out of 200 bytes and how do we show the user something before the image is fully loaded?

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!

There are a couple of cool things about this technique:

  1. It makes the perceived loading time wicked fast.
  2. It uses something traditionally costly in terms of performance to increase performance.
  3. It’s doable on the web.

Big header background images (and their performance drawbacks) are definitely something that we can relate to when building for the web, so this is useful stuff. We may try to avoid the downloading of heavy images, but sometimes we make the tradeoff to to achieve a certain mood. The best we can do in that situation is try to optimize the perceived performance, so we might as well steal this technique.

A working example

We’re going to recreate this header image feature with a sort of “critical CSS” approach. The very first request will load the tiny image in inline CSS, then the high-res background comes after first render.

It will look something like this when it’s done loading:

In this example, we are using a background image, regarding it as decoration rather than part of the content. There are some finer points to debate around when these types of images are to be regarded as content (and thus coded as an <img>) and when they are background images. In order to make use of smart sizing modes (like the CSS values cover and contain), background images are probably the most common solution for these kinds of designs, but new properties like object-fit are making the same approach a bit easier for content images. Sites like Medium already use blurred content images to improve load times, but the usefulness of that technique is debatable – do the blurred images bring anything to the table if the loading technique fails? Anyway: in this article, we’ll focus on this technique as applicable to background images.

Here’s the outline of how it’ll work:

  1. Inline a tiny image preview (40×22 pixels) as a base64-encoded background image inside of a <style>-tag. The style tag also includes general styling and the rules for applying a gaussian blur to the background image. Finally, it includes styles for the larger version of the header image, scoped to a different class name.
  2. Get the URL to the large image from the inline CSS, and preload it using JavaScript. If the script fails for some reason, no harm no foul – the blurred background image is still there, looking pretty cool.
  3. When the large image is loaded, add a class name that toggles the CSS to use the large image as the background while removing the blur. Hopefully, the blur-removal part can also be animated.

You can find the final example as a Pen. You’ll likely see the blurred image for a moment before the sharper image loads. If not, try reloading the page with an empty cache.

A tiny, optimized image

First of all, we need a preview version of the image. Facebook got the size of theirs down to 200 bytes via compression voodoo (like storing the non-changing JPEG header bits in the app), but we’re not going to get quite that extreme. With a size of 40 by 22 pixels, this particular image comes in at around 1000 bytes after running it through some image optimization software.

The full-size JPEG image is around 120Kb at 1500 × 823 pixels. That file size could probably be a lot lower, but we’ll leave that as is, since this is a proof of concept. In a real-world example, you would probably have a few size variations of the image and load a different one based on viewport size – heck, maybe even load a different format like WebP.

The filter function for images

Next, we want to scale the tiny image up to cover the element, but we don’t want it looking pixelated and ugly. This is where the filter()-function comes in. Filters in CSS might seem a little confusing, as there are effectively three kinds: the filter property, its proposed backdrop-filter counterpart (in the Filter Effects Level 2 spec) and finally the filter() function for images. Let’s take a look at the property first:

.myThing {
  filter: hue-rotate(45deg);
}

One or more filters are applied, each operating on the result of the previous – a lot like a list of transforms. There’s a whole range of predefined filters that we can use: blur(), brightness(), contrast(), drop-shadow(), grayscale(), hue-rotate(), invert(), opacity(), sepia() and saturate().

What’s even cooler is that this is a spec shared between CSS and SVG, so not only are the predefined filters specced in terms of SVG, we can also create our own filters in SVG and reference them from CSS:

.myThing {
  filter: url(myfilter.svg#myCustomFilter);
}

The same filter effects are valid in backdrop-filter, applying them when compositing a transparent element with its backdrop – perhaps most useful for creating the “frosted glass” effect.

Finally, there’s the filter() function for image values. The idea is that anywhere that you can reference an image in CSS, you should also be able to pipe it through a list of filters. For the tiny header image, we inline it as a base64 dataURI and run it through the blur() filter.

.post-header {
  background-image: filter(url(data:image/jpeg;base64,/9j/4AAQ ...[truncated] ...), blur(20px));
}

This is great, since this is exactly what we’re looking for when recreating the technique from the Facebook app! There’s bad news on the support front though. The filter property is supported in the latest versions of all browsers except IE, but none of them except WebKit have implemented the filter() function part of the spec.

When I say WebKit here, I mean the WebKit nightly builds at the time this post is written, and not Safari. The filter function for images is technically in iOS9 as -webkit-filter(), but this hasn’t been reported anywhere official as far as I can find, which is a bit weird. The reason is probably that it has a horrible bug with background-size: the original image is not resized, but the filtered output tile size is. This breaks background image functionality pretty bad, especially with blurring. It has been fixed, but not in time to make it into the Safari 9 release, so I guess they didn’t want to announce that feature.

But what do we do with the missing/broken filter() functionality? We could either give browsers that don’t support it a solid background until the image loads, although that means they’ll get no background at all if JS fails to load. Boring!

No, we’ll save the filter() function as an extra spice for animating the swapped-in image later, and instead emulate the filter function for the initial image with SVG.

Recreating the blur filter with SVG

Since the spec handily provides an SVG equivalent for the blur()-filter, we can recreate how the blur filter works in SVG, with a few tweaks:

  • The edges get a bit semi-transparent when applying the gaussian blur. We can fix this by adding something called a feComponentTransfer filter. The component transfer allows you to manipulate each color channel (including alpha) of a source graphic. This particular variation uses the feFuncA element, which maps any value between 0 and 1 in the alpha channel to 1, meaning it removes any alpha transparency.
  • The color-interpolation-filters attribute on the <filter> element must be set to sRGB. SVG filters default to using the linearRGB color space, and CSS operates in sRGB. Most browsers seem to handle the color corrections right, but Safari/WebKit makes the colors all washed out unless this value is set.
  • The filterUnits is set to userSpaceOnUse, which in simplified terms means that coordinates and lengths (like the stdDeviation of the blur) maps to pixels in the element we apply the blur to.

The resulting SVG code looks something like this:

<filter id="blur" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
    <feGaussianBlur stdDeviation="20" edgeMode="duplicate" />
    <feComponentTransfer>
      <feFuncA type="discrete" tableValues="1 1" />
    </feComponentTransfer>
</filter>

The filter property uses its own url() function where we can either reference, or URI-encode an SVG filter. So how do we apply a filter to to something inside of a background-image: url(...)?

Well, SVG files can point to other images, and we can apply filters to those images inside the SVG. The problem is that SVG background images can’t fetch any outside resources. But we can get around this by base64-encoding the JPG inside the SVG. This wouldn’t be feasible with a large image, but for our tiny one it should be fine. The SVG will look like this:

<svg xmlns="http://www.w3.org/2000/svg"
     xmlns:xlink="http://www.w3.org/1999/xlink"
     width="1500" height="823"
     viewBox="0 0 1500 823">
  <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/4AAQSkZJ ...[truncated]..."
         x="0" y="0"
         height="100%" width="100%"/>
</svg>

Another downside (compared to just using the filter()-function with a bitmap) is that we’ll need to manually set some sizes for the SVG to cooperate properly with the background size. The SVG itself has a viewBox set to mimic the aspect ratio of the image, and the width and height properties are set to the same measurements to make sure it works cross-browser (for example, IE screws up the aspect ratio if these are missing). Finally, the <image>-element is set to cover the entire SVG canvas.

Now we can use this file as a background to the post header, and it’ll look something like this:

As a final step, we can put the SVG wrapper image inline in the CSS to avoid an extra request. Inline SVG needs to be URI encoded, I use yoksel’s SVG encoder for this. So now we have a dataURI containing another dataURI. DataURInception!

When encoding the SVG we get some text to paste into the url(), but it’s worth noting that we need to prepend some metadata to make it display: data:image/svg+xml;charset=utf-8,. The charset-stuff is important: it makes the encoded SVG play nicely across browsers.

.post-header {
  background-color: #567DA7;
  background-size: cover;
  background-image: url(data:image/svg+xml;charset=utf-8,%3Csvg...);
}

At this point, the whole page, including the image, is 1 request and 5KB when using GZIP.

Getting the URL to the big image

Next, we create a rule for the enhanced header, where we set up the huge background image.

.post-header-enhanced {
  background-image: url(largeimg.jpg);
}

Instead of just toggling the class name, thus triggering the big image to load, we want to preload the big image and then apply the class name. This is so that we can smoothly animate the switch later, being reasonably sure the large image is done loading. Since we don’t want to hard-code the image URL in both the CSS and the JavaScript, we’ll grab the URL using JavaScript from inside the styles. As the class name is not yet applied, we can’t just look at headerElement.style.backgroundImage etc – it doesn’t know about the background yet. To solve this, we’ll use the CSSOM – the CSS Object Model, and the read-only JS properties that let us traverse the CSS rules.

The following snippet finds the class name for the enhanced header, then grabs the URL with some regex. After that it preloads the image and triggers the added class name once that’s done.

<script>
window.onload = function loadStuff() {
  var win, doc, img, header, enhancedClass;
  
  // Quit early if older browser (e.g. IE 8).
  if (!('addEventListener' in window)) {
    return;
  }
  
  win = window;
  doc = win.document;
  img = new Image();
  header = doc.querySelector('.post-header');
  enhancedClass = 'post-header-enhanced';

  // Rather convoluted, but parses out the first mention of a background
  // image url for the enhanced header, even if the style is not applied.
  var bigSrc = (function () {
    // Find all of the CssRule objects inside the inline stylesheet 
    var styles = doc.querySelector('style').sheet.cssRules;
    // Fetch the background-image declaration...
    var bgDecl = (function () {
      // ...via a self-executing function, where a loop is run
      var bgStyle, i, l = styles.length;
      for (i=0; i<l; i++) {
        // ...checking if the rule is the one targeting the
        // enhanced header.
        if (styles[i].selectorText &&
            styles[i].selectorText == '.'+enhancedClass) {
          // If so, set bgDecl to the entire background-image
          // value of that rule
          bgStyle = styles[i].style.backgroundImage;
          // ...and break the loop.
          break; 
        }
      }
      // ...and return that text.
      return bgStyle;
    }());
    // Finally, return a match for the URL inside the background-image
    // by using a fancy regex I Googled up, as long as the bgDecl 
    // variable is assigned at all.         
    return bgDecl && bgDecl.match(/(?:\(['|"]?)(.*?)(?:['|"]?\))/)[1];
  }());

  // Assign an onLoad handler to the dummy image *before* assigning the src
  img.onload = function () {
    header.className += ' ' +enhancedClass;
  };
  // Finally, trigger the whole preloading chain by giving the dummy
  // image its source.
  if (bigSrc) {
    img.src = bigSrc;
  }
};
</script>

The script quits early if addEventListener is not supported, which should overlap nicely with the rest of the support needed. As far as I can tell, all reasonably modern SVG-supporting browsers support the rest of the CSSOM and other JavaScript features used.

Animating the swap

It’s a bit of a bummer that we didn’t get to use the filter()-function, after finding out that it exists and all. So we’ll add an animated effect, when swapping in the high-res image. This only works in WebKit nightlies at the moment, and we can safely use the @supports-rule to scope the changes. Here’s an animated GIF to show the effect in action:

Note that we can’t use a transition for this: the filter()-function is animatable, but only for changing values in the filter chain – when the background image changes, we’re out of luck. We can, however, use an animation for this, but it does mean that we need to repeat the URL to the background image two more times, as the start and end values. A small price to pay.

Here’s the CSS for the enhanced header styles for browsers that do understand the filter()-function:

@supports (background-image: filter(url('i.jpg'), blur(1px))) {
  .post-header {
    transform: translateZ(0);
  }
  .post-header-enhanced {
    animation: sharpen .5s both;
  }
  @keyframes sharpen {
    from {
      background-image: filter(largeimg.jpg), blur(20px));
    }
    to {
      background-image: filter(largeimg.jpg), blur(0px));
    }
  }
}

One final detail is the translateZ(0)-trick on the header here: without it, the animation is crazy jerky. I tried being all modern and used will-change: background-image, but that didn’t persuade the browser to create a hardware-backed layer, so I had to go with the old trick of adding a 3D “null transform”.

Fast, progressively-enhanced background images

There we have it, a page with a humongous background image (albeit blurry) loading in 5Kb, lazy-loading the sharp looking full-size image. Right now, only WebKit can animate the sharper image in, but I’m hopeful that other browsers will implement the filter()-function soon. I’m sure there’s lots more fun techniques we can use it for.