Understanding and Using IntersectionObserver for Lazy Loading, Scroll Animations, Infinite Scrolling, and Virtual Lists
This article introduces the IntersectionObserver API, explains its constructor, options, and entry properties, and demonstrates practical applications such as lazy loading images, scroll‑triggered animations, infinite scrolling, and virtual list rendering with complete code examples.
Introduction
Historically, implementing lazy loading, scroll animations, and similar effects required manual scroll event handling, offset calculations, and debouncing. Modern browsers provide observation APIs that simplify and improve performance when tracking an element’s intersection with the viewport.
Overview of IntersectionObserver
The IntersectionObserver API creates an observer object that monitors the intersection between a target element and a root (usually the viewport). When the intersection ratio crosses specified thresholds, a callback receives detailed data about the change.
API
Constructor
The constructor accepts two arguments:
callback : Function executed when intersection changes.
options (optional): Configuration object.
The observer instance provides four methods:
observe(element) – start observing a target.
unobserve(element) – stop observing a target.
disconnect() – disconnect the observer.
takeRecords() – return an array of IntersectionObserverEntry objects.
const myObserver = new IntersectionObserver(callback, options);
myObserver.observe(element);
myObserver.unobserve(element);
myObserver.disconnect();Constructor Parameters
- callback
The callback receives entries (an array of IntersectionObserverEntry ) and the observer instance.
- options
Options allow customizing the root, rootMargin, and threshold list.
Property
Description
root
Ancestor element used as the viewport; defaults to the document viewport.
rootMargin
Margin around the root for expanding or shrinking the intersection area (e.g., "0px 0px 0px 0px").
threshold
Sorted list of ratios (0‑1) that trigger the callback when crossed; default is 0.
IntersectionObserverEntry
Property
Description
boundingClientRect
Bounding box of the target element (same as
element.getBoundingClientRect()).
intersectionRatio
Proportion of the target visible in the root.
intersectionRect
Rectangle describing the intersecting area.
isIntersecting
Boolean indicating whether the target is currently intersecting.
rootBounds
Bounding box of the root.
target
The observed element.
time
Timestamp when the intersection occurred.
Applications
Lazy Loading
Store the real image URL in data-src and replace src when the element becomes visible.
<div class="skin_img">
<img class="lazyload" data-src="//example.com/image.jpg" alt="Example" />
</div>
.skin_img { margin-bottom:20px; width:auto; height:500px; overflow:hidden; position:relative; } const imgList = [...document.querySelectorAll('img')];
const observer = new IntersectionObserver((entries) => {
entries.forEach(item => {
if (item.isIntersecting) {
console.log(item.target.dataset.src);
item.target.src = item.target.dataset.src;
observer.unobserve(item.target);
}
});
}, { root: document.querySelector('.root') });
imgList.forEach(img => observer.observe(img));Scroll Animation
Add animation classes when elements intersect the viewport.
const elements = document.querySelectorAll('.observer-item');
const observer = new IntersectionObserver(callback);
elements.forEach(ele => {
ele.classList.add('opaque');
observer.observe(ele);
});
function callback(entries, instance) {
entries.forEach(entry => {
if (entry.isIntersecting) {
const element = entry.target;
element.classList.remove('opaque');
element.classList.add('come-in');
instance.unobserve(element);
}
});
}
/* CSS */
.come-in { opacity:1; transform:translateY(0); animation:come-in 1s ease forwards; }
@keyframes come-in { 100% { transform:translateY(0); } }Infinite Scrolling
Observe a sentinel element ( lastContentRef ) and load more data when it becomes visible.
const [list, setList] = useState(new Array(10).fill(null));
const [loading, setLoading] = useState(false);
const lastContentRef = useRef(null);
const loadMore = async () => {
if (timer) return;
setLoading(true);
await new Promise(resolve => timer = setTimeout(() => resolve(timer = null), 1500));
setList(prev => [...prev, ...new Array(10).fill(null)]);
setLoading(false);
};
useEffect(() => {
const io = new IntersectionObserver((entries) => {
if (entries[0]?.isIntersecting && !loading) loadMore();
});
lastContentRef?.current && io.observe(lastContentRef.current);
}, []);Virtual List
Use rootMargin to create a buffer zone and render only items that intersect the viewport, replacing off‑screen items with placeholders.
<template v-for="(item, idx) in listData" :key="item.id">
<div class="content-item" :data-index="idx">
<template v-if="item.visible">
{{ item.value }}
</template>
</div>
</template>
_entries.forEach(row => {
const index = row.target.dataset.index;
if (!row.isIntersecting) {
row.target.style.height = `${row.target.clientHeight}px`;
listData.value[index].visible = false;
} else {
row.target.style.height = '';
listData.value[index].visible = true;
}
});Compatibility
Most modern browsers support IntersectionObserver; only legacy Internet Explorer lacks native support.
Conclusion
IntersectionObserver provides a concise, performant way to monitor element‑viewport intersections, enabling lazy loading, scroll‑based animations, infinite scrolling, virtual lists, analytics, parallax effects, and auto‑play scenarios. Its low overhead and straightforward API make it a valuable tool for front‑end developers.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.