Progressive Image Loading

Progressive Image Loading

First impressions matter. Images are an integral part of how you view a website, but can sometimes be slow to load and display. Unfortunately, this is very true for slow networks. So, what can be done to solve this issue?

Medium, an online publishing platform, has a very interesting approach. So, I went for a closer look at it!

Progressive Image Loading Medium throttled to 3G network speed (750 kb/s)

Medium uses a blur effect before fully loading the images on their website pages. Looking at the source code, the structure of their images look like this:

<div class="progressiveMedia js-progressiveMedia graf-image is-canvasLoaded is-imageLoaded" data-image-id="#" data-width="1920" data-height="1080" data-scroll="native">
  <img src="#" crossorigin="anonymous" class="progressiveMedia-thumbnail js-progressiveMedia-thumbnail">
  <canvas class="progressiveMedia-canvas js-progressiveMedia-canvas" width="75" height="22"></canvas>
  <img class="progressiveMedia-image js-progressiveMedia-image" data-src="#" src="#">
  <noscript class="js-progressiveMedia-inner">&lt;img class="progressiveMedia-noscript js-progressiveMedia-inner" src="#"&gt;</noscript>
</div>

Progressive Image Loading

Each image (aka “progressiveMedia”) is enclosed in a <div> element. These images have two copies, higher and lower quality versions. Presumably, the lower quality version would load FIRST, be blurred to hide the artifacts from resizing/compression, and THEN the higher quality one will be showed once it is fully loaded.

In order to recreate this effect, we need the two file versions. I used a Python script to resize the images to a maximum of 42 px x 42 px dimension (sweet spot according to Facebook), while keeping its aspect ratio. The code recursively goes through each directory and finds *.jpg files. Copies of these are then resized, and renamed to *_thumbnail.jpg.

Image Resize:

import os, sys, fnmatch
from PIL import Image

size = 42, 42
matches = []

for root, dirnames, filenames in os.walk(sys.argv[1]):
    for filename in fnmatch.filter(filenames, '*.jpg'):
        matches.append(os.path.join(root, filename))
print matches

for infile in matches:
    outfile = os.path.splitext(infile)[0] + "_thumbnail.jpg"
    if infile != outfile:
        try:
            im = Image.open(infile)
            im.thumbnail(size, Image.ANTIALIAS)
            im.save(outfile, "JPEG")
        except IOError:
            print "Cannot create thumbnail for '%s'" % infile

Sample Run:

$ python resize.py <DIRECTORY>

Progressive Image Loading

Obviously, there are improvements that could be made but thankfully the script managed to keep the files under 1 KB. While doing some trial and error, I found it tedious to delete files manually. So, here is a simple Bash script to automate that for you:

Deleting Files:

$ find <DIRECTORY> -type f -name '*_thumbnail.jpg' -exec rm {} +

Loading Mechanism:


// Replace all img tags with placeholders
var all = document.getElementsByTagName('img');
for (i = all.length - 1; i >= 0; i--) {
  var newimg;
  var smallName = all[i].src;

  if(smallName.indexOf(".jpg") > -1) {
    smallName = smallName.split(".jpg");
    smallName = smallName[0] + "_thumbnail.jpg";

    (newimg = document.createElement('div'));
    newimg.className = "placeholder";
    newimg.setAttribute("data-large", all[i].src)
    newimg.innerHTML = '<img src="' + smallName + '"class="img-small"><div style="padding-bottom: 66.6%;"></div>';
    all[i].parentNode.replaceChild(newimg, all[i]);
  }
}

// Standard loading
var placeHolders = document.querySelectorAll('.placeholder');
var lowResImages = [];
var hiResImages = [];

for (i = 0; i < placeHolders.length; i++) {
  var small = placeHolders[i].querySelector('.img-small');

  lowResImages.push(small.src);
  hiResImages.push(placeHolders[i].dataset.large);
}

function loader(items, job, allDone) {
  if (!items) {
    return;
  }

  if ("undefined" === items.length) {
    items = [items];
  }

  var count = items.length;

  var jobCompleted = function(items, i) {
    count--;
    if (0 == count) {
      allDone(items);
    }
  };

  for (var i = 0; i < items.length; i++) {
    job(items, i, jobCompleted);
  }
}

function loadLowRes(items, i, onComplete) {
  var onLoad = function(e) {
    e.target.removeEventListener("load", onLoad);

    // 1: Load small image
    placeHolders[i].querySelector('.img-small').classList.add('loaded');

    onComplete(items, i);
  }
  var img = new Image();
  img.addEventListener("load", onLoad, false);
  img.src = items[i];
}

function loadHighRes(items, i, onComplete) {
  var onLoad = function(e) {
    e.target.removeEventListener("load", onLoad);

    // 2: Load large image
    var imgLarge = new Image();
    imgLarge.src = placeHolders[i].dataset.large;
    imgLarge.classList.add('loaded');
    placeHolders[i].appendChild(imgLarge);

    onComplete(items, i);
  }
  var img = new Image();
  img.addEventListener("load", onLoad, false);
  img.src = items[i];
}

loader(lowResImages, loadLowRes, function() {
  loader(hiResImages, loadHighRes, function() {
    console.log("Loaded");
  });
});

Stylesheet:

.placeholder {
  background-color: #f6f6f6;
  background-size: cover;
  background-repeat: no-repeat;
  position: relative;
  overflow: hidden;
  display: block;
  max-width: 100%;
  margin: 0 0 1rem;
  border-radius: 5px;
}

.placeholder img {
  position: absolute;
  opacity: 0;
  top: 0;
  left: 0;
  width: 100%;
  transition: opacity 1s linear;
}

.placeholder img.loaded {
  opacity: 1;
}

.img-small {
  filter: blur(100px);
}

After we have generated the files needed, it is time to implement the loading mechanism. The code takes in every single <img> tag and replaces it with the same structure that you have already seen above. Then, after it has replaced the appropriate tags, images are then loaded using a standard loader. Blurred lower quality photos are loaded first, then every single higher quality ones are loaded. Once all of the higher quality images are loaded already, then they are showed on the page.


  Recommend