Placeholders for lazy loaded content

Orde Saunders' avatarPublished: by Orde Saunders

Lazy loading images and other content is a good way to improve the perceived load speed of a site and also allows for some client side content negotiation to load appropriate assets. The disadvantage of this approach is that the page layout may reflow as new content is added. To minimise the effect of this we can add placeholders into which the content is loaded.

What's the problem?

In order to reduce the amount we have load to get our content onto a screen as fast as possible a useful technique is to lazy load supplementary (i.e. non core) content after the browser has enough information to display the core content. This has several benefits including allowing us to use client side logic to only load content for which there is sufficient space, bandwidth or capabilities. A classic example for this is images: we want to minimise the data transferred so we can detect for screen size and bandwidth and load an appropriately sized image. We don't want to send the largest images to all devices on the off chance it's a large, high density, screen attached to a super fast connection but, when it is, we don't want to just send the smallest images.

When it comes to delivering the page, a fluid layout is ideal for coping with varied screen sizes but it can mean that when the content alters in size the layout alters to cope with this new size. If we're waiting until after the browser has enough information to draw the initial layout before we load some of our content then we are likely to trigger a layout reflow as this new content is brought in.

An example of this can be seen on the BBC responsive sites. In the initial page they are not including images but are using JavaScript to read data attributes on <div> elements that get replaced by the appropriate image. On a page with a lot of images this can lead to a significant change in layout that is obvious to users. Shown below is a comparison of a page as it appears after the browser has initially rendered it and it has become interactive and the same page at the same scroll position as the images are loading.

Layout before images are lazy loadedLayout after images are lazy loaded

As can be seen, the layout - and in particular the vertical position of individual elements - has changed significantly. This is particularly pronounced on a slow connection and, depending on the timing of the arrival of new content, can lead to a situation where you attempt to select a particular item only to find it has moved and you inadvertently select the item that has taken its place.

What's the solution?

In order to minimise the effect of this kind of layout reflow we need to provide some sort of placeholder that will set aside an area into which the late arriving content will be placed. With fluid layouts it may not be possible to accurately size this but a reasonable approximation will do a lot to reduce the impact. If we are using client side logic to determine when and what to load this gives us an opportunity to set a CSS hook at a high level (such as on the <html> or <body> elements) and use this to size our placeholders. It's probably easiest to demonstrate this with a worked example.

Example

To start with we have our content item marked up using an empty div with a data attribute acting as target for the lazy loading image replacement.

<article class="content-item">
  <div class="image-container">
    <div data-image-replacement="true" data-image-src="/path/to/image"></div>
  </div>
  <div class="content-container">
    <h1>Lorem ipsum</h1>
    <p>Dolor sit amet...</p>
  </div>
</article>

We initially style this on the assumption that there will be no image present:

.content-item {
  width: 100%;
  margin-bottom: 1em;
}
.image-container {
  width: 0;
  float: left;
}
.content-item img {
  width: 100%;
}
.content-container {
  width: 100%;
  float: right;
}

Then we do our lazy loading test in JavaScript:

if ([image lazy load conditions are met]) {
  document.querySelector('html').classList.add('lazy-images');
  document.addEventListener('DOMContentLoaded', function () {
    var els = document.querySelectorAll('div[data-image-replacement]');
    for (var i = 0; i < els.length; i += 1) {
      // Replace <div> with <img>.
    }
  });
}

The key here is that the first thing we do is set the CSS hook. This allows us to immediately apply styles to set placeholders for the images, long before the browser will load them and trigger the layout reflow.

.lazy-load .image-container {
  width: 25%;
}
.lazy-load .content-container {
  width: 75%;
}

We now have the image and the content in containers that are the final sizes which means the layout will not need to be reflowed when the images are actually loaded.