How to use IntersectionObserver for lazy loading images in JavaScript
Stop using scroll event listeners for image loading. Use IntersectionObserver to detect when images enter the viewport and load them only then. This guide shows you how to implement it in vanilla JavaScript and modern frameworks.
Stop using scroll event listeners or data-attributes to trigger image loading. They cause performance bottlenecks and memory leaks on large pages. Use the IntersectionObserver API to detect when images enter the viewport and load them only then. This guide shows you how to implement it in vanilla JavaScript and modern frameworks like React or Vue.
Prerequisites
- A modern browser that supports the IntersectionObserver API (Chrome 51+, Firefox 52+, Safari 12.1+, Edge 16+).
- Basic knowledge of HTML and JavaScript ES6+ syntax.
- A local development environment with a web server (e.g., Node.js with http-server or PHP built-in server).
Step 1: Create the HTML structure with placeholder elements
Create an HTML file with a list of images. Use a placeholder attribute or a CSS class to mark images that need lazy loading. Do not set the src attribute for these images initially. Instead, use a data attribute like data-src to hold the real image URL.
<!-- images-container.html -->
<div class="gallery">
<img data-src="https://picsum.photos/400/300?random=1" class="lazy" alt="Image 1">
<img data-src="https://picsum.photos/400/300?random=2" class="lazy" alt="Image 2">
<img data-src="https://picsum.photos/400/300?random=3" class="lazy" alt="Image 3">
<img data-src="https://picsum.photos/400/300?random=4" class="lazy" alt="Image 4">
<img data-src="https://picsum.photos/400/300?random=5" class="lazy" alt="Image 5">
</div>
You will see a list of images where the actual source is hidden until the user scrolls to them. The data-src attribute holds the URL you want to load later. The lazy class helps you target these elements in JavaScript.
Step 2: Define the IntersectionObserver options
Configure the observer to watch for elements that enter the viewport. Set the threshold to 0.1 to trigger when 10% of the image is visible. Set rootMargin to "-100px" to start loading images slightly before they hit the bottom of the screen.
const options = {
root: null, // null means the viewport
rootMargin: '-100px', // Start loading 100px before the element hits the viewport
threshold: 0.1 // Trigger when 10% of the image is visible
};
This configuration ensures images start loading before they become visible, creating a smooth user experience without a flash of unoptimized content.
Step 3: Create the observer callback function
Define a callback function that runs when an image enters or exits the viewport. Check the isIntersecting property to see if the image is visible. If it is intersecting, update the src attribute to match the data-src attribute. Remove the lazy class to prevent duplicate loading attempts.
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');
observer.unobserve(img); // Stop observing this image after it loads
}
});
}, options);
The code stops observing the image once it has loaded to save memory. This prevents the browser from re-checking the same image every time the user scrolls.
Step 4: Initialize the observer for all images
Select all images with the lazy class and pass them to the observer. Use document.querySelectorAll to get the NodeList of elements. Iterate through the NodeList and call observer.observe for each image.
const lazyImages = document.querySelectorAll('img.lazy');
if ('IntersectionObserver' in window) {
lazyImages.forEach(img => imageObserver.observe(img));
} else {
// Fallback for older browsers
lazyImages.forEach(img => {
img.src = img.dataset.src;
img.classList.remove('lazy');
});
}
Check if the API exists before using it. If it does not exist, load the images immediately. This ensures your site works on older browsers like Internet Explorer or very old mobile devices.
Verify the installation
Open your browser developer tools and go to the Console tab. Run the following command to check if the IntersectionObserver is available.
console.log('IntersectionObserver available:', typeof IntersectionObserver !== 'undefined');
You should see IntersectionObserver available: true. Now open your HTML file in a browser. Scroll down the page. You will see the images load only when they approach the bottom of the viewport. The network tab in the developer tools will show the images requesting data only when needed.
Troubleshooting
Error: "img is not defined" or "Cannot read property 'src' of null"
This error occurs if you try to access the src attribute on an element that was not found or was removed from the DOM. Ensure you are selecting elements with document.querySelectorAll before the DOM is fully loaded. Add the script at the bottom of the <body> tag or use the DOMContentLoaded event.
document.addEventListener('DOMContentLoaded', () => {
const lazyImages = document.querySelectorAll('img.lazy');
// ... rest of code
});
Error: Images never load or load twice
If images never load, check your rootMargin value. If it is too large, the observer might miss the element before it triggers. If images load twice, you might be missing the observer.unobserve(img) line in your callback. Always remove the observer after the first load to prevent duplicate requests.
Performance issue: High memory usage
If your page has thousands of images, creating an observer for each one might be heavy. Instead, create a single observer instance and observe multiple elements at once. The IntersectionObserver API is designed to handle many elements efficiently, but ensure you unobserve elements once they are loaded to keep memory usage low.
Fallback for older browsers
Some users might use older browsers that do not support IntersectionObserver. Always provide a fallback that loads images immediately if the API is missing. This ensures your site remains functional for everyone.
if (!('IntersectionObserver' in window)) {
const lazyImages = document.querySelectorAll('img.lazy');
lazyImages.forEach(img => {
img.src = img.dataset.src;
img.classList.remove('lazy');
});
}
By implementing this fallback, you guarantee that your site works on all devices without breaking the layout or functionality.