Lazy Loading CSS Background Images

A screenshot of a codepen demonstrating lazy loading.

I ran into a little issue lately where I was trying to optimize the performance of a website by making all of the images on the front page use lazy loading.

For a traditional <img /> tag, this is pretty trivial. Just add the loading="lazy" attribute onto the tag.

However, the majority of the images I was working on were images that were loaded using the  CSS background-image property, which meant traditional HTML wouldn't cut it.

Why This Was A Problem

When you use loading="lazy" on img tags, it makes it so your images won't be rendered on the page until the user has gotten close to seeing them via scrolling. This makes it so you can cut out the extraneous load time and extra requests that would typically accompany an image-heavy page and only load what you need, when you need it (if at all).

How I Resolved It

I learned about the IntersectionObserver API in JavaScript, which has pretty good coverage across all modern browsers. The Intersection Observer is similar in spirit to the Mutation Observer API, except instead of watching for changes or "mutations" in the DOM, this API monitors when different elements are about to intersect. 

In particular, you can use this to detect when an element is about to enter the viewport.

The key to making this useful is to create a CSS class that sets the background-image to none !important, and then using JS to remove that class when the element is about to scroll into view.

Demo

See the Pen Lazy Loading CSS Background Images by Andrew Benbow (@OulipianSummer) on CodePen.

Explanation

In the demo above, you really won't notice anything special. As you scroll down the page, you should see a column of images. It doesn't look like anything magical is happening, and this is the point. If you comment out the imageObserver.observe(image); line at about ~28, then the whole thing will break. 

If you actually open up the Demo in codepen.io, and open up the console, you will see a series of console messages that get logged as you scroll down the page, showing that these images are getting loaded on an as-needed basis instead of all at once.

Image
Screenshot of the intersection observer API in action.

The thing that is powering this from the JS side of the house is this piece of code here.

// Remove the class that hides the background.
const showImageBackground = (node) => {
  node.classList.remove("lazybg");
  // Debug
  console.log("image shown!");
};

// Give the image observer some parameters for when to display an image.
const backgroundOptions = {
  threshold: 0,
  rootMargin: "0px 0px 50px 0px"
};

// An intersection observer that can detect when an element is (or about to be) in view.
const imageObserver = new IntersectionObserver((entries, imageObserver) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      showImageBackground(entry.target);
      imageObserver.unobserve(entry.target);
    }
  });
}, backgroundOptions);

// Driver code.
document.addEventListener("DOMContentLoaded", () => {
  const bgimages = document.querySelectorAll(".lazybg");
  bgimages.forEach((image) => {
    imageObserver.observe(image);
  });
});

The important part is the imageObserver variable, which is mapped to an instance of IntersectionObserver for each background image on your page. 

The showImageBackground function gets called whenever the viewport gets close enough to it. How close is close enough to trigger the image to load?

Take a look at the backgroundOptions variable. The rootMargin property defines a margin (similar to a CSS margin) around the viewport. When the element you're observing enters that 50px boundary on the bottom of the viewport, the entry.isIntersecting check becomes true, and the image gets rendered on the page, triggering your browser to fetch the resource from the server (if needed).

Importantly, the element becomes unobserved using imageObserver.unobserve() to ensure that this logic doesn't get triggered again.

Comments

Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.

About Me

I am a writer-turned-web-developer-turned-writer-again. My blog focuses on experimental literature, constrained writing prompts, and original works of poetry. I am also currently writing a mystery novel. Join me here to talk about all things fringe writing, nontraditional novels, and more.