Lazy Loading Images with JavaScript IntersectionObserver
The IntersectionObserver API provides a performant,
asynchronous mechanism for detecting when an element enters or leaves
the browser viewport, making it the ideal solution for lazy loading
images. By deferring image requests until elements are actually needed
on screen, web applications can significantly reduce initial page load
times, decrease bandwidth usage, and eliminate performance bottlenecks
traditionally caused by scroll event listeners.
How the IntersectionObserver API Works
Historically, lazy loading required attaching listeners to
scroll, resize, or
orientationchange events and manually calculating element
positions using methods like getBoundingClientRect(). This
approach frequently caused layout thrashing and forced synchronous
reflows on the browser’s main thread.
The IntersectionObserver API resolves this by delegating
visibility detection to the browser’s internal rendering pipeline. It
registers a callback function that executes whenever a target element
intersects an ancestor element or the top-level document viewport,
completely decoupling visibility checks from the main execution
thread.
Implementing Lazy Loading Step-by-Step
Implementing lazy loading with IntersectionObserver
involves structuring the HTML markup to hold the image source
temporarily, configuring the observer, and swapping the source when the
image becomes visible.
1. HTML Markup Structure
Instead of placing the actual image URL in the src
attribute, store it inside a data-src attribute. This
prevents the browser from immediately downloading the image during the
initial HTML parse.
<img class="lazy-image" data-src="large-image.jpg" alt="Description" src="placeholder.jpg" />2. Creating the Observer Instance
Instantiate a new IntersectionObserver by passing a
callback function and an optional configuration object:
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.remove('lazy-image');
observer.unobserve(img);
}
});
}, {
root: null,
rootMargin: '200px 0px',
threshold: 0.01
});3. Attaching Target Elements
Query all deferred image elements in the document and pass each one
to the observer’s observe() method:
document.addEventListener('DOMContentLoaded', () => {
const lazyImages = document.querySelectorAll('.lazy-image');
lazyImages.forEach(image => imageObserver.observe(image));
});Key Configuration Options
The IntersectionObserver constructor accepts an
options object with three main properties:
root: The element used as the viewport for checking visibility. Passingnulldefaults to the browser’s actual viewport.rootMargin: A set of offsets (similar to CSS margin) that expands or contracts the root’s bounding box. For example, settingrootMargin: '200px'allows images to begin loading 200 pixels before they enter the screen, providing a seamless experience for users without showing blank spaces.threshold: A single number or array of numbers between0.0and1.0, indicating the percentage of target visibility required to trigger the callback. A value of0triggers execution as soon as even one pixel enters the viewport.
Cleaning Up with
unobserve()
Once an image source is swapped and loading begins, calling
observer.unobserve(img) on that specific element stops
monitoring it. This ensures that the callback does not execute
unnecessarily on subsequent scrolls, keeping memory and CPU usage to a
minimum.