A man standing next to a wall, seen through some glasses from far away

Creating a progressive image loader

  • Kristofer Giltvedt Selbekk

Notice that hero image? It's 2 megabytes big. That's more than 6 times the rest of the bytes required for you to visit this site!

Now, images are nice, and quality photos are a big part of an amazing user experience, but that doesn't mean you have to download a few megs of images just to get a first impression!

It's day 12 (yeah, I really write most of these articles the day before), and I realized the load time on that first hero image was terrible. I'm going to fix that for this site throughout this article - and you can do the same for your site or app.

Started from the bottom

The first step of a great image loading component is to show a really compressed version of the image to start with, and then load the high quality version in the background. This way, it progressively improves the image quality!

Let's first look at a mega-simple Image component:

class Image extends React.Component {
    render() {
        return <img {...this.props} />;
    }
}

Now we up

In order to load a more high-res version of the image, we need to pass a high resolution URL, and load it once the low-res version has been displayed. Let's pass two props - src and highResSrc, and switch over to the high res version once it's loaded.

class Image extends React.Component {
  state = {
    highResLoaded: false,
  };
  componentDidMount() {
    // High res versions should be optional!
    if (!this.props.highResSrc) {
      return;
    }
    const image = new Image();
    image.addEventListener(
      'load',
      () => this.setState({
        highResLoaded: true,
      })
    );
    img.src = this.props.highResSrc;
  }
  render() {
    const {
      src,
      highResSrc,
      ...rest,
    } = this.props;
    return (
      <img
        src={this.state.highResLoaded ? highResSrc : src}
        {...rest}
      />
    );
  }
}

This way, we can upgrade our image significantly after the first load.

Is this the best we can do?

In 2019, we're going to see a lot of improvements to the React API. One of the most anticipated additions is what's known as Suspense.

With Suspense, we can specify a fallback component while we wait for some promise to resolve. Perhaps that promise could be an image loading?

Let's sketch this up as well:

import React from 'react';
import { unstable_createResource } from 'react-cache'; // very beta

const ImageResource = unstable_createResource(
    src =>
        new Promise((resolve, reject) => {
            const image = new Image();
            image.onload = resolve;
            image.onerror = reject;
            image.src = src;
        })
);

const HighResImage = props => {
    ImageResource.read(props.src);
    return <img {...props} />;
};

function ProgressiveImage(props) {
    return (
        <React.Suspense fallback={<img src={props.src} alt={props.alt} />}>
            <HighResImage src={props.highResSrc} alt={props.alt} />
        </React.Suspense>
    );
}

As you can tell, this isn't stable or production ready yet, but it's probably similar to something we'll see in a React version not too far away.

You can play with a live example of the code above here.

By creating progressive image components like this, you'll improve your user experience on all mediums - whether it's slow mobile connections or fast desktop ones. There are, of course, tons of improvements left to do, but this seems like a great first start.