On

by m4rrc0

Lazy loading images, so easy you won’t believe it (a native strategy)


tl;dr

The code is pretty easy to grasp. Read the comments and you should be good to go.

<div class="img-lazy-wrapper"> <!-- We need a wrapper for the placeholder while image lazy loads -->
  <img
    src="/assets/lazy-bear@528w.96b21f25.png" <!-- Compress your image -->
    alt="A lazy bear" <!-- Don't forget the alt attribute -->
    loading="lazy" <!-- Native browser lazy loading -->
    width="528" <!-- Specify width and height so the browser can allocate the appropriate space for the image -->
    height="560"
    class="img-lazy"
    onload="this.parentNode.style.backgroundColor = 'transparent';this.style.opacity = 1;">
		<!-- Transition: Fade the image in and the background out -->
</div>

<style>
.img-lazy {
  opacity: 0; /* Start with image hidden */
  transition: opacity 1s; /* Transition to make the appearance smoother */
  max-width: 100%; /* Avoid image punching out of the screen  */
  height: auto; /* Necessary to keep aspect ratio if width is reduced by max-width */
}
.img-lazy-wrapper {
  width: -webkit-fit-content;
  width: -moz-fit-content;
  width: fit-content; /* Make the wrapper borders stick to the image. Otherwise, would stretch horizontally */
  transition: background-color 1s; /* Make disappearance smoother */
  background-color: var(--color-img-placeholder, rgba(200, 200, 200, 0.3)) /* Basic placeholder */
}
</style>

<noscript><style>
/* Overrides opacity and background-color in case we have no JS to get them it back */
.img-lazy { opacity: 1; }
.img-lazy-wrapper { background-color: transparent }
</style></noscript>

Intro

Us developers really like to reinvent the wheel. Lazy loading images has been a topic of choice to do that.

Nevertheless, we couldn't easily find an elegant, native, lightweight solution we like to implement by default on poko. So here is our solution for now.

Our prerequisites

A solution

  1. Compress images

  2. Determine the dimensions of each image

  3. Activate native lazy loading in browser

  4. Progressively enhance with some styles and two lines of javaScript.

1. Compress images

There are many ways to do it and your development pipeline might have a mature native solution already.

If you are not a tech person and you are not sure your CMS or website builder handles it for you, there are many tools online to compress images.

In our case, using Astro, we simply used astro-imagetools to compress images automatically while importing them. We might improve this later, for example generating multiple image sizes and formats, reducing the load even further for modern browsers. Currently, it checks our box though.

2. Determine the dimensions of each image

This is necessary to make use of some native browser image handling. Providing width and height attributes to <img> tags allows the browser to reserve space for your image before it loads. It is necessary to avoid having content jump around when images load. It used to not be enough to set these up but now it is.

Again, there are multiple ways to get dimensions and your development pipeline might just provide this information by default. In case it doesn't, we have found a nice and small npm package called probe-image-size. It allows for 'probing' an image locally or remotely.

import probeImageSize from "probe-image-size";

const imageData = await probeImageSize(url);

console.log(imageData);
// {
//   url: 'https:// ...',
//   width: 528,
//   height: 560,
//   type: 'png',
//   mime: 'image/png',
//   wUnits: 'px',
//   hUnits: 'px',
//   length: 47951
// }

3. Activate native lazy loading in browser

This is the easy part. Simply set the attribute on your HTML <img> element: <img ... loading="lazy" ... >

The HTML for the image becomes:

<img
  src="/assets/lazy-bear@528w.96b21f25.png"
  alt="A lazy bear"
  loading="lazy"
  width="528"
  height="560">

4. Progressively enhance

Now in modern browsers, we have got a compressed image that is lazy loaded and an allocated space on the page before that. That is great!

Now we'd like to create a transition effect so that visitors understand that something is loading on the reserved space.

On the image itself, we can use the onload attribute to 'do something' when the image is loaded.

The simplest placeholder we can think of is color. So let's color up the reserved space while the image is loading then fade out the color and fade in the image.

First some styles on the <img> and a wrapper <div> for the color.

<style>
.img-lazy {
  opacity: 0;
  transition: opacity 1s;
  max-width: 100%;
  height: auto;
}
.img-lazy-wrapper {
  width: -webkit-fit-content;
  width: -moz-fit-content;
  width: fit-content;
  transition: background-color 1s;
  background-color: var(--color-img-placeholder, rgba(200, 200, 200, 0.3))
}
</style>

Some explanations:

Note: we would very much like to avoid the wrapper <div> but could not find an elegant alternative. Please share if you have one.

Our HTML becomes

<div class="img-lazy-wrapper">
  <img
    src="/assets/lazy-bear@528w.96b21f25.png"
    alt="A lazy bear"
    loading="lazy"
    width="528"
    height="560"
    class="img-lazy"
    onload="this.parentNode.style.backgroundColor = 'transparent';this.style.opacity = 1;">
</div>

Note the onload attribute on the <img> tag. That is the only javaScript we need to make the transition from placeholder to image.

Of course, if we rely on javaScript to display the image, we should have a fail safe mechanism for visitors without JS. In this case, we simply remove the transition effect.

<noscript><style>
/* Overrides opacity and background-color in case we have no JS to get them it back */
.img-lazy { opacity: 1; }
.img-lazy-wrapper { background-color: transparent }
</style></noscript>

Compatibility and final thoughts

According to my limited research (and the caniuse website), around 80% of users (in May 2022) should benefit from the whole technique. 20% of visitors will have a limited experience, either image is not lazy loaded, or content is jumping on image load, or the wrapping div will be wider than the image, or there will be no background transition effect... something like that. In any case, they won't have trouble seeing the content and that is the point.

We love solutions like these which are lightweight and use the platform as much as possible. Technical debt is reduced to a minimum. It makes them future proof and compatibility ratio will only increase with time.

Moreover, with poko we are not forcing this on anyone since using this component is optional.