Images - Lazy loading

What if images don't arrive? A tale of a badly designed lazy loader

If you’re looking for an example of exactly what not to do in terms of front-end performance, I can’t think of a better one than this - they threw away a lot of the performance optimizations browsers give us for free in a bizarre attempt at improving page loading, which ended up doing the opposite:

I was recently conducting some exploratory work for a potential client when I hit upon a pretty severe flaw in a design decision they’d made: They’d built a responsive image lazyloader in JavaScript which, by design, worked by:

  1. immediately applying display: none; to the <body>;
  2. waiting until the very last of the page’s images had arrived;
  3. once they’d arrived, removing the display: none; and gradually fading the page into visibility.

Not only does this strike me as an unusual design decision—setting out to build a lazyloader and then having it intentionally block rendering—there had been no defensive strategy to answer the question: what if something goes wrong with image delivery?

‘Something wrong’ is exactly what happened. Due to an imperfect combination of:

  1. images being completely unoptimised, plus;
  2. a misconfiguration with their image transformation service leading to double downloads for all images;

…they’d managed to place 27.9MB of images onto the Critical Path. Almost 30MB of previously non-render blocking assets had just been turned into blocking ones on purpose with no escape hatch. Start render time was as high as 27.1s over a cable connection1.

If you’re going to build an image loader that hides the whole page until all images are ready, you must also ask yourself what if the images don’t arrive?

Preventing downloading images or objects until they are visible in the viewport - WHATWG

This is an interesting discussion on the possibility of standardizing a way to request that the browser not load or delay loading images or objects. The key point that developers from the BBC stress is that for a non-zero number of users, JavaScript fails to run yet is enabled, so having a way to ensure they can still view images is important. As excellent as they can be, relying on on JavaScript solutions and serving non-functional markup can lead to broken pages in those cases since we don’t truly control our webpages.

See also: A standard way to lazy load images - WICG

Prevent image loading with MutationObserver

Edit: this is not actually reliable due to the way that various browsers will try to pre-emptively fetch resources as they parse an HTML document, as confirmed by Jake Archibald of the Chrome dev team.

It turns out that if you place a MutationObserver in the <head> (or presumably even in the <body> before any <img> elements), and have it start observing right away it will be able to remove the src before the browser downloads the image, thus allowing for lazy loading and other optimizations.

I put this in <head> and normal <img> tags in <body>. - No image requests visible in Firefox dev tools - Safari shows requests but 0 bytes transferred - Chrome seems to get 4 kb over the wire before calling it quits

Code language: JavaScript

const observer = new MutationObserver(mutations => {
  mutations.forEach(mutation => {
    Array.from(mutation.addedNodes)
      .filter(node => node.tagName === 'IMG')
      .forEach(img => {
        img.dataset.src = img.src;
        img.src = '';
      });
  });
});
 
observer.observe(document.documentElement, {
  childList: true,
  subtree: true
});

Real-World Uses for MutationObserver

MutationObserver is a lesser known JavaScript feature which allows you to detect when elements in a web page are inserted, changed or removed. It is still relatively new, but it is supported by every modern browser.

The web is full of demos and tutorials of MutationObserver, but it’s pretty hard to find examples of it actually being used in practice. Even a search of Github is almost all libraries and test cases. We’ve had a couple occasions to use it at Eager however, which I now have the opportunity to share.

Client-side Image Optimization

Believe it or not, it’s actually possible to swap the src’s of img tags before the browser begins to load them. We can use that to optimize our images without changing the HTML source of our page. This code uses a FireSize service to handle the actual optimization.

We start by setting up a MutationObserver which will call our checkNode function with any new nodes which are added to the DOM:

Code language: JavaScript

var observer = new MutationObserver(function(mutations){
  for (var i=0; i < mutations.length; i++){
    for (var j=0; j < mutations[i].addedNodes.length; j++){
      checkNode(mutations[i].addedNodes[j]);
    }
  }
});
 
observer.observe(document.documentElement, {
  childList: true,
  subtree: true
});

If we run this code early in the head of the page, it will call our checkNode function with each DOM node as the browser parses the page’s HTML. This gives us the ability to check or mutate these nodes before they’ve ever been rendered.

We can define our checkNode function to decide if this is an image for us to optimize.

Code language: JavaScript

checkNode = function(addedNode) {
  if (addedNode.nodeType === 1 && addedNode.tagName === 'IMG'){
    addedNode.src = optimizeSrc(addedNode.src)
  }
}

Finally, we can define optimizeSrc to switch out our image’s src for an optimized one:

Code language: JavaScript

optimizeSrc = function(src) {
  return "//firesize.com/" + src;
}

For a complete implementation, take a look at our FireSize app source code.

Initializing When An Element Becomes Available on the Page

It’s a common pattern to wait for jQuery.ready or DOMContentLoaded to initialize code which depends on elements on the page. Those events don’t fire until the entire DOM has loaded however, meaning the page will start to be rendered before you have a chance to change or add to its content.

Our pattern from the image optimization solution also works for detecting when any element becomes available, allowing you to initialize code which depends on that element at the exact first moment it’s possible. We can redefine checkNode to instead check if our element matches an arbitrary selector:

Code language: JavaScript

checkNode = function(addedNode) {
  if (addedNode.nodeType === 1){
    if (addedNode.matches('.should-underline')){
      SmartUnderline.init(addedNode);
    }
  }
}

Front-End Performance Checklist 2017

This is an incredibly exhaustive list of performance tweaks, improvements, and best practices. Some may be outside the scope of smaller websites, but there are plenty of things for everyone.

Back in the day, performance was often a mere afterthought. Often deferred till the very end of the project, it would boil down to minification, concatenation, asset optimization and potentially a few fine adjustments on the server’s config file. Looking back now, things seem to have changed quite significantly.

Performance isn’t just a technical concern: It matters, and when baking it into the workflow, design decisions have to be informed by their performance implications. Performance has to be measured, monitored and refined continually, and the growing complexity of the web poses new challenges that make it hard to keep track of metrics, because metrics will vary significantly depending on the device, browser, protocol, network type and latency (CDNs, ISPs, caches, proxies, firewalls, load balancers and servers all play a role in performance).

So, if we created an overview of all the things we have to keep in mind when improving performance — from the very start of the process until the final release of the website — what would that list look like? Below you’ll find a (hopefully unbiased and objective) front-end performance checklist for 2017 — an overview of the issues you might need to consider to ensure that your response times are fast and your website smooth.

How Medium does progressive image loading

Recently, I was browsing a post on Medium and I spotted a nice image loading effect. First, load a small blurry image, and then transition to the large image. I found it pretty neat and wanted to dissect how it was done.

[…]

I have performed a WebPageTest test against this page on Medium where you can see how it loads too. And if you want to see it by yourself, open Medium’s post in your browser, disable the cache and throttle the response so it takes longer to fetch the images and you can see the effect.

Here is what is going on:

  1. Render a div where the image will be displayed. Medium uses a <div/> with a padding-bottom set to a percentage, which corresponds to the aspect ratio of the image. Thus, they prevent reflows while the images are loaded since everything is rendered in its final position. This has also been referred to as intrinsic placeholders.

  2. Load a tiny version of the image. At the moment, they seem to be requesting small JPEG thumbnails with a very low quality (e.g. 20%). The markup for this small image is returned in the initial HTML as an <img/>, so the browser starts fetching them right away.

  3. Once the image is loaded, it is drawn in a <canvas/>. Then, the image data is taken and passed through a custom blur() function You can see it, a bit scrambled, in the main-base.bundle JS file. This function is similar, though not identical, to StackBlur‘s blur function. At the same time, the main image is requested.

  4. Once the main image is loaded, it is shown and the canvas is hidden.

All the transitions are quite smooth, thanks to the CSS animations applied.