How to Use Intersection Observer for Smooth and Efficient Lazy Loading

Posted by

Ditch manual scroll listeners. Here’s how to load images, data, and components only when they enter the viewport.

Ditch manual scroll listeners. Here’s how to load images, data, and components only when they enter the viewport.

Introduction

You know that feeling when a website loads instantly as you scroll, images fade in smoothly, data appears right when you need it, and everything feels effortless?

That’s not luck. That’s the Intersection Observer API doing its magic behind the scenes.

Instead of running expensive scroll event handlers 60 times per second, this modern browser API tells you exactly when an element enters or leaves the viewport, so you can lazy-load content efficiently.

It’s how modern sites like Medium, Unsplash, and YouTube achieve buttery-smooth performance.

Let’s break down exactly how to use it and why it’s the only lazy loading strategy you need in 2025.


1️. The Old Way: Manual Scroll Events

Before Intersection Observer, lazy loading meant:

window.addEventListener("scroll", () => {
const rect = img.getBoundingClientRect();
if (rect.top < window.innerHeight && rect.bottom > 0) {
loadImage();
}
});

❌ Constant event firing
❌ Manual math
❌ Poor performance on mobile

As soon as you had dozens of images or an infinite scroll, performance dropped, and scrolling stuttered.

The old approach worked, but it was like checking every second if your pizza is ready instead of letting the oven tell you.


2️. The Modern Way: Intersection Observer

With the Intersection Observer API, you can simply observe elements and get notified when they become visible.

Here’s a minimal setup:

const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
console.log("Element is visible:", entry.target);
}
});
});

observer.observe(document.querySelector(".target"));

✅ Automatically detects visibility
✅ Handles scroll, resize, and zoom all for you
✅ Runs on a separate thread for better performance


3️. Lazy Loading Images (Real Example)

Let’s build a real-world lazy loader for images.

HTML

<img data-src="high-res.jpg" alt="Lazy image" class="lazy" />
<img data-src="another.jpg" alt="Lazy image" class="lazy" />

JavaScript

const images = document.querySelectorAll("img.lazy");

const observer = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.add("loaded");
observer.unobserve(img); // Stop observing once loaded
}
});
});

images.forEach((img) => observer.observe(img));

CSS (Optional Fade-In Effect)

img {
opacity: 0;
transition: opacity 0.5s ease;
}
img.loaded {
opacity: 1;
}

✅ Loads images only when visible
✅ Saves bandwidth
✅ Adds a smooth fade-in effect


4️. Example: Infinite Scrolling Data

Want to load more items when users reach the bottom of a list?

<div id="posts"></div>
<div id="sentinel"></div> <!-- Watch this element -->
const posts = document.getElementById("posts");
const sentinel = document.getElementById("sentinel");

const observer = new IntersectionObserver(async (entries) => {
if (entries[0].isIntersecting) {
const data = await fetchMorePosts();
renderPosts(data);
}
});

observer.observe(sentinel);

✅ Fires only when the sentinel element appears
✅ No need to check scroll position
✅ Perfect for infinite feeds or lazy APIs


5️. Example: Triggering Animations on Scroll

Want elements to fade in as they appear?
 Easy.

<div class="box hidden">Hello</div>
<div class="box hidden">World</div>
const boxes = document.querySelectorAll(".box");

const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("show");
}
});
});

boxes.forEach((box) => observer.observe(box));
.box {
opacity: 0;
transform: translateY(30px);
transition: all 0.6s ease;
}
.box.show {
opacity: 1;
transform: translateY(0);
}

✅ Smooth scroll-triggered animations
✅ No external libraries (like ScrollMagic or GSAP triggers)


6️. Tuning Visibility with rootMargin and threshold

You can control when your observer fires:

const observer = new IntersectionObserver(callback, {
root: null, // viewport
rootMargin: "100px", // preload before it’s visible
threshold: 0.1, // fire when 10% of element is visible
});

rootMargin It’s great for preloading images early
threshold helps control when animations start

Think of it like “how close do you want the element to be before acting?”


7️. Observing Multiple Elements Efficiently

The beauty of Intersection Observer is that one observer can track dozens of elements:

const observer = new IntersectionObserver(handleIntersection);

document.querySelectorAll(".lazy").forEach((el) => observer.observe(el));

✅ Lightweight, no need for one listener per element
✅ The browser handles batching internally

This is why it’s far more efficient than onscroll.


8️. Debugging and Visualization

You can easily debug with DevTools → “Rendering” → check “Highlight scroll performance issues.”
 Or temporarily log intersection ratios:

entries.forEach((entry) => {
console.log(entry.target, entry.intersectionRatio);
});

This helps you tune your thresholds and understand when triggers occur.


9️. Real-World Use Cases

✅ Intersection Observer powers many modern frameworks’ internals, even React Lazy and Next.js use similar concepts under the hood.


10. Browser Support

✅ Supported in all modern browsers (Chrome, Edge, Firefox, Safari).
 ⚠️ For IE11 or very old browsers, you can use a small polyfill.

In 2025, you can safely use it everywhere without fear.


Conclusion

The Intersection Observer API is the modern way to build smooth, efficient, and performant scroll-based experiences.

✅ No heavy scroll events
✅ Works for images, lists, animations
✅ Saves bandwidth and improves UX

Once you try it, you’ll never go back to manual scroll tracking again.

Start small, lazy load a few images, and you’ll instantly feel how smooth your app becomes.


Call to Action

Have you used Intersection Observer in your projects?
Share your favorite use case or performance tip in the comments 👇

And if your teammate still checks scrollTop manually…
Send them this post before they crash another page

Leave a Reply

Your email address will not be published. Required fields are marked *