How to Build Your Own Progressive Image Loader

Share this article

A magician revealing an image via progressive image loader

Key Takeaways

  • A progressive image loader enhances user experience by loading a low-resolution image first, which is then replaced by a high-resolution image when the element comes into view. The technique can be created using HTML5, CSS3, and JavaScript. It is lightweight, has no dependencies, supports responsive images, and works in all modern browsers.
  • The HTML structure for the progressive image loader involves a link container with a class of ‘progressive replace’ and an image source with a class of ‘preview’. The CSS defines the link container styles, sizes the images according to the container width, blurs the preview image, and defines styles and animations for the full image when revealed. JavaScript checks if the required browser APIs are available before adding a load event listener to the page.
  • The progressive image loader also supports the HTML5 image srcset and sizes attributes, which define multiple images at different sizes and resolutions. The browser then selects the most appropriate version for the device. Enhancements to consider include checking horizontal scroll, replacing progressive images added to a page using JavaScript, and improving Firefox performance.
A magician revealing an image via progressive image loader

You may have encountered progressive images on Facebook and Medium. A blurred low-resolution image is replaced with a full-resolution version when the element is scrolled into view:

progressive image example

The preview image is tiny – perhaps a 20px width highly-compressed JPEG. The file can be less than 300 bytes and appears instantly to give the impression of fast loading. The real image is lazy-loaded when required.

Progressive images are great but the current solutions are quite complex. Fortunately, we can build one with a little HTML5, CSS3 and JavaScript. The code will:

  • be fast and lightweight – just 463 bytes of CSS and 1,007 bytes of JavaScript (minified)
  • support responsive images to load alternative versions for larger or high-resolution (Retina) screens
  • have no dependencies – it will work with any framework
  • work in all modern browsers (IE10+)
  • be progressively enhanced to work in older browsers or when JavaScript or image loading fails
  • be easy to use.

Our Demo and GitHub Code

Here’s what our technique will look like:

See the Pen responsive-image by SitePoint (@SitePoint) on CodePen.


Download the code from GitHub

The HTML

We’ll start with some basic HTML to implement our progressive image:

<a href="full.jpg" class="progressive replace">
  <img src="tiny.jpg" class="preview" alt="image" />
</a>

where:

  • full.jpg is our large full-resolution image contained in the link href, and
  • tiny.jpg is our tiny preview image.

We already have a minimal working system. Without any JavaScript – or in older browsers where it may fail – the user can view the full image by clicking the preview.

Both images must have the same aspect ratio. For example, if full.jpg is 800 x 200, it has a resulting aspect ratio of 4:1. tiny.jpg could therefore be 20 x 5 but you should not use a 30px width which would require a fractionally impossible 7.5px height.

Note the classes used on the link and the preview image; these will be used as hooks in our JavaScript code.

To Inline or Not Inline Images

The preview image can also be inlined as a data URI, e.g.

<img src="data:image/jpeg;base64,/9j/4AAQSkZJ..."  class="preview" />

Inlined images appear instantly, require fewer HTTP requests and avoid additional page reflows. However:

  • it takes more effort to add or change an inline image (although build processes such as Gulp can help)
  • base-64 encoding is less efficient and is typically 30% larger than binary data (although this is offset by additional HTTP request headers)
  • inlined images cannot be cached. They will be cached in the HTML page but could not be used on another page without re-sending the same data.
  • HTTP/2 lessens the need for inline images.

Be pragmatic: inlining is a good option if the image is used on a single page or the resulting code is small, i.e. not much longer than a URL!

The CSS

We start by defining the link container styles:

a.progressive {
  position: relative;
  display: block;
  overflow: hidden;
  outline: none;
}

This sets the essential container layout properties. The link can have other classes and styles applied to set the dimensions or position if required.

You could consider setting the exact dimensions or using the padding-top trick to enforce an intrinsic aspect ratio. This would ensure the container is sized before the image loads and reflows are avoided. However, it would be necessary to calculate the size and/or width:height ratios for every image. I’ve chosen to keep it simple:

  • the preview and large images must have an identical aspect ratio (see above)
  • the preview image will define the height of the container almost instantly because it is in-lined or loads quickly.

Again, be pragmatic. Performance will improve if you define container widths and heights but will be especially noticeable on pages with lots of images, e.g. a gallery (where all images are likely to have the same aspect ratio anyway).

The replace class on the container is removed when the full image loads and the click is disabled. Therefore, we can remove the standard link pointer:

a.progressive:not(.replace) {
  cursor: default;
}

The preview and large images within the container are sized according to the container width:

a.progressive img {
  display: block;
  width: 100%;
  max-width: none;
  height: auto;
  border: 0 none;
}

Note that height: auto is required or IE10/11 may miscalculate the image’s height.

The preview image is blurred using a length of 2vw which ensures the blur amount looks similar regardless of the page dimensions. The overflow: hidden applied to the container provides a hard edge to the image. It is also scaled by 1.05 to prevent the page background color showing through the blurred outer edge of the image. This also means we can use a pleasing zoom effect to reveal the full image.

a.progressive img.preview {
  filter: blur(2vw);
  transform: scale(1.05);
}

Finally, we define styles and animations for the full image when it is revealed:

a.progressive img.reveal {
  position: absolute;
  left: 0;
  top: 0;
  will-change: transform, opacity;
  animation: reveal 1s ease-out;
}

@keyframes reveal {
  0% {transform: scale(1.05); opacity: 0;}
  100% {transform: scale(1); opacity: 1;}
}

The full image is positioned above the preview then has the opacity increased from 0 to 1 and the scale changed from 1.05 to 1 over one second. You can apply other transformation and/or filter effects as required.

The JavaScript

We’re practising responsible progressive enhancement so the JavaScript code initially checks whether the required browser APIs are available before adding a load event listener to the page:

// progressive-image.js
if (window.addEventListener && window.requestAnimationFrame && document.getElementsByClassName) window.addEventListener('load', function() {

The load event is fired when the page and all assets have completed loading. We do not want large images to start loading before essential resources such as fonts, CSS, JavaScript and preview images are ready (which could happen if we used the DOMContentLoaded events which fires when the DOM is ready).

Next, we fetch all image container elements with the class names progressive and replace:

var pItem = document.getElementsByClassName('progressive replace'), timer;

getElementsByClassName() returns a live array-like HTMLCollection which changes as associated elements are added and removed from the page. The benefits will become apparent shortly.

Next we’ll define an inView() function which determines whether each container is in the viewport by comparing its getBoundingClientRect position with the window.pageYOffset vertical scroll position:

// image in view?
function inView() {
  var wT = window.pageYOffset, wB = wT + window.innerHeight, cRect, pT, pB, p = 0;
  while (p < pItem.length) {

    cRect = pItem[p].getBoundingClientRect();
    pT = wT + cRect.top;
    pB = pT + cRect.height;

    if (wT < pB && wB > pT) {
      loadFullImage(pItem[p]);
      pItem[p].classList.remove('replace');
    }
    else p++;
  }
}

When the container is in view, its node is passed to a loadFullImage() function and the replace class is removed. This instantly removes the node from the pItem HTMLCollection so the container is never re-processed again.

The loadFullImage() function creates a new HTML Image() object and sets its values as necessary, i.e. by copying the container’s href to the src attribute and applying a reveal class:

// replace with full image
function loadFullImage(item) {
  if (!item || !item.href) return;

  // load image
  var img = new Image();
  if (item.dataset) {
    img.srcset = item.dataset.srcset || '';
    img.sizes = item.dataset.sizes || '';
  }
  img.src = item.href;
  img.className = 'reveal';
  if (img.complete) addImg();
  else img.onload = addImg;

An internal addImg function is called once the image has loaded:

// replace image
  function addImg() {
    // disable click
    item.addEventListener('click', function(e) { e.preventDefault(); }, false);

    // add full image
    item.appendChild(img).addEventListener('animationend', function(e) {
      // remove preview image
      var pImg = item.querySelector && item.querySelector('img.preview');
      
      if (pImg) {
        e.target.alt = pImg.alt || '';
        item.removeChild(pImg);
        e.target.classList.remove('reveal');
      }
    });
  }
}

This code:

  • disables the click event on the container
  • appends the image to the page which starts the fade/zoom animation
  • waits for the animation to end using the animationend listener then copies the alt tag, deletes the preview image node and removes the reveal class from the full image. This step aids performance and also prevents some strange shearing problems when the Edge browser is resized.

Finally, we must call the inView() function to check whether any progressive image containers are visible on the page when it first runs:

inView();

We must also call the function when the page is scrolled or the browser is resized. Some older browsers (yes, primarily IE) can react to those events very rapidly so we’ll throttle the callback to ensure it cannot be called any more than once every 300 milliseconds:

window.addEventListener('scroll', scroller, false);
window.addEventListener('resize', scroller, false);

function scroller(e) {
  timer = timer || setTimeout(function() {
    timer = null;
    requestAnimationFrame(inView);
  }, 300);
}

Note the call to requestAnimationFrame which runs inView prior to the next repaint.

Responsive Images

The HTML5 image srcset and sizes attributes define multiple images at different sizes and resolutions. The browser then selects the most appropriate version for the device.

The code above supports this feature – add data-srcset and data-sizes attributes to the link container, e.g.

<a href="small.jpg"
  data-srcset="small.jpg 800w, large.jpg 1200w"
  data-sizes="100vw"
  class="progressive replace">
  <img src="preview.jpg" class="preview" alt="image" />
</a>

After loading, the full image code will be:

<img src="small.jpg"
    srcset="small.jpg 800w, large.jpg 1200w"
    sizes="100vw"
    alt="image" />

Modern browsers will load large.jpg when the viewport width is 800px or higher. Older browsers and those with a smaller viewport width will receive small.jpg. For more information, refer to How to Build Responsive Images with srcset.

Usage Notes

I’ve kept the code small but feel free to use or improve it on any project. Enhancements to consider:

  • Horizontal scroll checking. Only vertical scrolling is checked so all images in the horizontal plane are replaced.
  • Dynamically adding progressive images. Progressive images added to a page using JavaScript are only replaced when a scroll or resize event occurs.
  • Firefox performance. The browser can struggle when replacing large images — you may see a noticeable flicker.

Please tweet me @craigbuckler if you find it useful.

Frequently Asked Questions (FAQs) on Building Your Own Progressive Image Loader

What is the purpose of a progressive image loader?

A progressive image loader is a tool that enhances the user experience on a website by loading images progressively. This means that instead of waiting for the entire image to load, users see a low-quality version of the image first, which gradually improves in quality. This technique is particularly useful for websites with heavy images, as it significantly improves page load times and keeps users engaged.

How does a progressive image loader work?

A progressive image loader works by first loading a low-quality or smaller version of an image, often blurred, and then gradually replacing it with the full-quality image. This process is achieved through JavaScript, which manipulates the DOM to replace the low-quality image with the high-quality one once it’s fully loaded.

How can I implement a progressive image loader in my website?

Implementing a progressive image loader in your website involves a few steps. First, you need to create a low-quality version of each image on your site. Then, you need to write JavaScript code that will load these low-quality images first and replace them with the high-quality images once they’re fully loaded. This process can be simplified by using libraries such as progressive-image.js.

What are the benefits of using a progressive image loader?

Using a progressive image loader can significantly improve the user experience on your website. By loading images progressively, you can reduce page load times and keep users engaged, even if your site contains heavy images. Additionally, progressive image loading can improve your site’s SEO, as search engines often penalize sites with slow load times.

Can I use a progressive image loader with React?

Yes, you can use a progressive image loader with React. There are several libraries available that make it easy to implement progressive image loading in a React application, such as react-progressive-image and react-graceful-image.

How does progressive image loading affect SEO?

Progressive image loading can have a positive impact on SEO. Search engines like Google prioritize sites that load quickly, and progressive image loading can significantly reduce page load times. Additionally, by improving the user experience, progressive image loading can increase the amount of time users spend on your site, which can also boost your SEO.

What is IntersectionObserver and how is it related to progressive image loading?

IntersectionObserver is a JavaScript API that can be used to detect when an element becomes visible in the viewport. This can be used in conjunction with progressive image loading to only load images when they’re about to become visible, further improving page load times and user experience.

Are there any drawbacks to using progressive image loading?

While progressive image loading can significantly improve page load times and user experience, it does require additional work to implement. You’ll need to create low-quality versions of each image on your site, and write JavaScript code to handle the image loading process. However, the benefits often outweigh these drawbacks.

Can I use a progressive image loader with other JavaScript frameworks?

Yes, you can use a progressive image loader with any JavaScript framework. The implementation details may vary depending on the framework, but the basic concept remains the same.

How can I test the effectiveness of my progressive image loader?

You can test the effectiveness of your progressive image loader by measuring your site’s load times before and after implementation. Tools like Google’s PageSpeed Insights can provide detailed information about your site’s performance and offer suggestions for improvement.

Craig BucklerCraig Buckler
View Author

Craig is a freelance UK web consultant who built his first page for IE2.0 in 1995. Since that time he's been advocating standards, accessibility, and best-practice HTML5 techniques. He's created enterprise specifications, websites and online applications for companies and organisations including the UK Parliament, the European Parliament, the Department of Energy & Climate Change, Microsoft, and more. He's written more than 1,000 articles for SitePoint and you can find him @craigbuckler.

image loaderpatrickcprogressive loading
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week