Stop using scroll events for visibility detection. Intersection Observer is performant, non-blocking, and runs on a separate thread.
Basic Usage
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add("visible");
observer.unobserve(entry.target); // Stop observing after first intersection
}
});
}, {
threshold: 0.1, // Trigger when 10% visible
rootMargin: "50px" // Start loading 50px before entering viewport
});
document.querySelectorAll(".lazy").forEach(el => observer.observe(el));Lazy Loading Images
function LazyImage({ src, alt, ...props }) {
const imgRef = useRef();
const [loaded, setLoaded] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
setLoaded(true);
observer.disconnect();
}
}, { rootMargin: "200px" });
if (imgRef.current) observer.observe(imgRef.current);
return () => observer.disconnect();
}, []);
return (
<div ref={imgRef}>
{loaded ? (
<img src={src} alt={alt} {...props} />
) : (
<div className="placeholder" />
)}
</div>
);
}Infinite Scroll
function InfiniteList({ fetchMore }) {
const sentinelRef = useRef();
useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) fetchMore();
});
if (sentinelRef.current) observer.observe(sentinelRef.current);
return () => observer.disconnect();
}, [fetchMore]);
return (
<div>
{items.map(renderItem)}
<div ref={sentinelRef} style={{ height: 1 }} />
</div>
);
}Scroll-Triggered Animations
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
entry.target.style.opacity = entry.intersectionRatio;
entry.target.style.transform = `translateY(${(1 - entry.intersectionRatio) * 20}px)`;
});
}, { threshold: Array.from({ length: 20 }, (_, i) => i / 20) }); // 20 thresholds for smooth animationPerformance Advantage
Unlike scroll event listeners, Intersection Observer runs asynchronously and doesn't block the main thread. No need for throttle/debounce.