Find out how to lazy load CSS background images to improve your website loading performance.
Every single HTTP request decreases loading performance. For a simple image, the attribute loading="lazy"
can be used in order to defer the loading of off-screen images until the image appears on the screen. Using lazy-loading we achieve at least 2 benefits over the traditional embedding in HTML.
- Loading speed – with lazy loading, the page is loaded faster because only images that are visible on the screen are being loaded.
- Rendering performance – less loading images reduces the time for page rendering.
That sounds good. However, while images can be lazy-loaded today then images loaded through CSS can’t. That doesn’t sound like we should give up at this point.
Set the goals for lazy loading CSS background
Let’s examine an example when you want to load the image as a background on a given HTML element:
<div style="background-image: url('example.webp');">Some example text</div>
Unfortunately, the background image will be loaded regardless of being on-screen or off-screen. With exception when element <div>
has defined display: none;
.
What we need is to stop loading the background image until it’s visible on the screen. Here we can use Intersection Observer API. What is it?
The Intersection Observer API provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document’s viewport.
The solution
Let’s then set up two goals:
- The solution should be fully automated. We want just to set an attribute
data-background-image="URL to image here"
on the HTML element. Example:<div data-background-image="example.webp">Some example text</div>
- If Intersection Observer API is not supported we simply load all background images. Note that polyfill for Intersection Observer is available for the browser that doesn’t support it, but for our simplification here we’ll skip this part and just allow the browser to load all background images.
Step 1: the code then would look like that:
const initialiseStyleBackgroundIntersectionObserver = () => { const lazyBackgrounds = Array.from(document.querySelectorAll('[data-background-image]')); if (lazyBackgrounds.length === 0) { return; } let lazyBackgroundObserver; const loadBackgroundIfElementOnScreen = (entry) => { if (entry.isIntersecting) { entry.target.style.backgroundImage = `url('${entry.target.dataset.backgroundImage}')`; lazyBackgroundObserver.unobserve(entry.target); } }; const observeElementVisibility = (lazyBackground) => { lazyBackgroundObserver.observe(lazyBackground); }; const setBackground = (element) => { element.style.backgroundImage = `url('${entry.target.dataset.backgroundImage}')`; }; if (typeof window.IntersectionObserver === 'function') { lazyBackgroundObserver = new IntersectionObserver((entries) => { entries.forEach(loadBackgroundIfElementOnScreen); }); lazyBackgrounds.forEach(observeElementVisibility); } else { lazyBackgrounds.forEach(setBackground); } };
Step 2: we need to run this code at a specific time. Here are the two options:
- When the DOM is created determined by
DOMContentLoaded
event. At this point, the initial HTML document has been completely loaded and parsed, without waiting for stylesheets, images, and subframes to finish loading. - When the page is fully loaded determined by
onload
event. At this point, all of the objects in the document are in the DOM, and all the images, scripts, links, and sub-frames have finished loading.
For our example we’ll use DOMContentLoaded
event:
if (typeof document.readyState === 'string' && document.readyState === 'complete') { initialiseStyleBackgroundIntersectionObserver(); } else { document.addEventListener('DOMContentLoaded', initialiseStyleBackgroundIntersectionObserver, true); }
Why that way? Why not just only document.addEventListener('DOMContentLoaded', initialiseStyleBackgroundIntersectionObserver, true);
. The reason is that your page might be loaded and parsed faster than you add the listener to the DOMContentLoaded
event and by that the code won’t run ever. To fix it we need to check first if the page is already loaded and parsed and decide about executing code immediately or adding an event listener.
Workable example
Here is a workable example of lazy loading CSS background images
Discussions
- Support lazy-loading CSS background-images – a discussion on WHATWG about implementing natively lazy loading backgrounds.
What, if we could use lazy loading directly in CSS? That would be possible with a little support from JavaScript. See example:
--background-image-lazy: prague-3010406_1920_thumbnail.jpeg; background-image: var(--background-image-lazy);
Above allows you to use it directly in the style sheet and a small script will take care of the rest.
Resources
- CSS Lazy Loading Background npm package.
- Demonstration.
This can be further improved by avoiding loading images, videos, and audio that are less than 1 second on the user’s screen. For example, during fast scrolling. Read more about improved lazy loading for image, video, and audio.