Intersection Observer API
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.
Before this API existed, detecting if an element was visible on the screen required binding to the scroll event and continuously calling getBoundingClientRect(). This was synchronous, ran on the main thread, and frequently caused massive performance bottlenecks (jank).
1. Why Intersection Observer?
- Performance: It is completely asynchronous. Computations are handled off the main thread, preventing UI blocking.
- Ease of Use: Replaces complex, math-heavy scroll event listeners with a clean, declarative API.
- Versatility: Ideal for lazy-loading images, infinite scrolling, scroll-triggered animations, and tracking ad visibility.
2. Basic Syntax
Creating an observer involves instantiating the IntersectionObserver class and passing a callback function and an optional configuration object.
// 1. Define the options
const options = {
root: null, // null means it uses the viewport
rootMargin: '0px', // margin around the root
threshold: 0.5 // 50% of the element must be visible
};
// 2. Define the callback
const callback = (entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
console.log('Element is visible!');
// Optional: stop observing once it's visible
// observer.unobserve(entry.target);
}
});
};
// 3. Create the observer
const observer = new IntersectionObserver(callback, options);
// 4. Target an element and observe it
const targetElement = document.querySelector('.animate-on-scroll');
observer.observe(targetElement);
3. Deep Dive into options
The options object configures when the observer's callback should be invoked.
root
The element that is used as the viewport for checking visibility of the target. Must be the ancestor of the target. Defaults to the browser viewport if not specified or if null.
rootMargin
Margin around the root. It serves to grow or shrink each side of the root element's bounding box before computing intersections.
- Example:
"10px 20px 30px 40px"(top, right, bottom, left). - Useful for lazy loading: setting
rootMargin: "500px 0px"will trigger the intersection 500px before the element actually enters the screen, allowing images to load in the background.
threshold
Either a single number or an array of numbers which indicate at what percentage of the target's visibility the observer's callback should be executed.
0: Callback triggers the moment a single pixel enters the viewport.1.0: Callback triggers when the entire element is visible.[0, 0.25, 0.5, 0.75, 1]: Triggers at 0%, 25%, 50%, 75%, and 100% visibility.
4. The IntersectionObserverEntry Object
When the callback fires, it receives an array of IntersectionObserverEntry objects. Key properties include:
entry.isIntersecting: Boolean.trueif the target is currently intersecting the root.entry.intersectionRatio: Float between 0.0 and 1.0 indicating how much of the target is visible.entry.boundingClientRect: Bounds of the target element.entry.intersectionRect: Bounds of the visible portion of the target element.entry.target: The DOM element being observed.
5. Practical Use Cases
Lazy Loading Images
const images = document.querySelectorAll('img[data-src]');
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src; // Swap placeholder with actual image
img.removeAttribute('data-src');
observer.unobserve(img); // Stop observing once loaded
}
});
}, { rootMargin: "100px" });
images.forEach(img => imageObserver.observe(img));
Infinite Scrolling
const sentinel = document.querySelector('#bottom-sentinel');
const loadMoreObserver = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
fetchMoreData(); // Your function to append more items to the DOM
}
});
loadMoreObserver.observe(sentinel);
6. Memory Management
Always remember to clean up your observers to prevent memory leaks, especially in Single Page Applications (React, Vue, Angular) when components unmount.
observer.unobserve(target): Stops watching a specific element.observer.disconnect(): Stops the observer entirely from watching any elements.