We use cookies to enhance your experience on the site
CodeWorlds

Web APIs: Intersection Observer and Resize Observer

Dr. Alan Grant stands before the panoramic window of the Jurassic Park control center. "Our monitors display hundreds of elements simultaneously — camera feeds, charts, sector maps," he says. "But loading everything at once kills performance. We need a way to react to what the user actually sees and how the page layout changes." The answer is Intersection Observer and Resize Observer — modern Web APIs for observing DOM elements.

The Problem: Manual Visibility Tracking

The traditional approach to checking whether an element is visible on screen requires listening to the

scroll
event and manually calculating positions:

1// Old approach — slow and complex
2window.addEventListener("scroll", () => {
3  const images = document.querySelectorAll("img[data-src]");
4
5  images.forEach(img => {
6    const rect = img.getBoundingClientRect();
7    const isVisible = rect.top < window.innerHeight && rect.bottom > 0;
8
9    if (isVisible) {
10      img.src = img.dataset.src; // Load the image
11    }
12  });
13});
14// Problem: the scroll event fires dozens of times per second!
15// Every getBoundingClientRect() call forces a reflow in the browser

Intersection Observer — Observing Visibility

IntersectionObserver
lets you asynchronously observe when an element enters or exits the visible area (viewport) or another element:

1// Creating an observer
2const observer = new IntersectionObserver((entries) => {
3  entries.forEach(entry => {
4    // entry.isIntersecting — whether the element is visible
5    // entry.intersectionRatio — what fraction of the element is visible (0-1)
6    // entry.target — the observed DOM element
7
8    if (entry.isIntersecting) {
9      console.log(`Element ${entry.target.id} is now visible!`);
10      console.log(`Visibility: ${(entry.intersectionRatio * 100).toFixed(0)}%`);
11    }
12  });
13}, {
14  // Configuration options
15  root: null,        // null = browser viewport
16  rootMargin: "0px", // margin around root (like CSS margin)
17  threshold: 0.5     // callback when 50% of element is visible
18});
19
20// Start observing
21const dinoCard = document.querySelector("#dino-card");
22observer.observe(dinoCard);
23
24// Stop observing
25// observer.unobserve(dinoCard);
26// observer.disconnect(); // Stops all observations

Lazy Loading Images

The most popular use of Intersection Observer is lazy loading — loading images only when the user scrolls to them:

1class LazyImageLoader {
2  constructor(options = {}) {
3    this.observer = new IntersectionObserver(
4      (entries) => this.handleIntersection(entries),
5      {
6        rootMargin: options.rootMargin || "200px", // Load 200px before entering view
7        threshold: 0
8      }
9    );
10  }
11
12  handleIntersection(entries) {
13    entries.forEach(entry => {
14      if (entry.isIntersecting) {
15        const img = entry.target;
16
17        // Replace placeholder with the real image
18        img.src = img.dataset.src;
19        img.classList.add("loaded");
20
21        // Stop observing the loaded image
22        this.observer.unobserve(img);
23
24        console.log(`Loaded image: ${img.dataset.src}`);
25      }
26    });
27  }
28
29  observe(selector) {
30    const images = document.querySelectorAll(selector);
31    images.forEach(img => this.observer.observe(img));
32    console.log(`Observing ${images.length} images`);
33  }
34
35  destroy() {
36    this.observer.disconnect();
37  }
38}
39
40// Usage
41const loader = new LazyImageLoader({ rootMargin: "300px" });
42loader.observe("img[data-src]");

Infinite Scroll

Intersection Observer is great for implementing infinite scrolling:

1class InfiniteScroll {
2  constructor(containerSelector, loadMore) {
3    this.container = document.querySelector(containerSelector);
4    this.loadMore = loadMore;
5    this.isLoading = false;
6    this.page = 1;
7
8    // Create a "sentinel" element at the end of the list
9    this.sentinel = document.createElement("div");
10    this.sentinel.className = "scroll-sentinel";
11    this.container.appendChild(this.sentinel);
12
13    // Observe the sentinel
14    this.observer = new IntersectionObserver(
15      (entries) => {
16        if (entries[0].isIntersecting && !this.isLoading) {
17          this.loadNextPage();
18        }
19      },
20      { rootMargin: "100px" }
21    );
22
23    this.observer.observe(this.sentinel);
24  }
25
26  async loadNextPage() {
27    this.isLoading = true;
28    console.log(`Loading page ${this.page}...`);
29
30    try {
31      const items = await this.loadMore(this.page);
32
33      items.forEach(item => {
34        const element = document.createElement("div");
35        element.className = "dino-card";
36        element.textContent = `${item.name} (${item.species})`;
37        // Insert BEFORE the sentinel
38        this.container.insertBefore(element, this.sentinel);
39      });
40
41      this.page++;
42    } catch (error) {
43      console.error("Loading error:", error);
44    } finally {
45      this.isLoading = false;
46    }
47  }
48}

Resize Observer — Observing Size Changes

ResizeObserver
notifies you when an element changes its dimensions. It is invaluable for responsive components:

1// Creating a Resize Observer
2const resizeObserver = new ResizeObserver((entries) => {
3  entries.forEach(entry => {
4    const { width, height } = entry.contentRect;
5    console.log(`Element ${entry.target.id} changed size: ${width}x${height}`);
6  });
7});
8
9// Observe an element
10const mapContainer = document.querySelector("#park-map");
11resizeObserver.observe(mapContainer);

Responsive Component with Resize Observer

1class ResponsiveDinoGrid {
2  constructor(containerSelector) {
3    this.container = document.querySelector(containerSelector);
4    this.currentLayout = null;
5
6    this.observer = new ResizeObserver((entries) => {
7      const entry = entries[0];
8      const width = entry.contentRect.width;
9      this.updateLayout(width);
10    });
11
12    this.observer.observe(this.container);
13  }
14
15  updateLayout(width) {
16    let newLayout;
17
18    if (width < 400) {
19      newLayout = "single-column";
20      this.container.style.gridTemplateColumns = "1fr";
21    } else if (width < 800) {
22      newLayout = "two-columns";
23      this.container.style.gridTemplateColumns = "1fr 1fr";
24    } else {
25      newLayout = "multi-column";
26      this.container.style.gridTemplateColumns = "repeat(auto-fill, minmax(250px, 1fr))";
27    }
28
29    if (newLayout !== this.currentLayout) {
30      this.currentLayout = newLayout;
31      console.log(`Layout changed to: ${newLayout} (width: ${width}px)`);
32    }
33  }
34
35  destroy() {
36    this.observer.disconnect();
37  }
38}

Combining Both Observers

1// Jurassic Park monitoring panel system
2class MonitoringPanel {
3  constructor(panelSelector) {
4    this.panel = document.querySelector(panelSelector);
5    this.cameras = [];
6
7    // Intersection Observer — enable/disable cameras based on visibility
8    this.visibilityObserver = new IntersectionObserver(
9      (entries) => this.handleVisibility(entries),
10      { threshold: 0.1 }
11    );
12
13    // Resize Observer — adjust image quality based on panel size
14    this.resizeObserver = new ResizeObserver(
15      (entries) => this.handleResize(entries)
16    );
17  }
18
19  addCamera(cameraElement) {
20    this.cameras.push(cameraElement);
21    this.visibilityObserver.observe(cameraElement);
22    this.resizeObserver.observe(cameraElement);
23  }
24
25  handleVisibility(entries) {
26    entries.forEach(entry => {
27      const cameraId = entry.target.dataset.cameraId;
28
29      if (entry.isIntersecting) {
30        console.log(`Camera ${cameraId}: ACTIVE (visible)`);
31        entry.target.classList.add("active");
32      } else {
33        console.log(`Camera ${cameraId}: PAUSED (out of view)`);
34        entry.target.classList.remove("active");
35      }
36    });
37  }
38
39  handleResize(entries) {
40    entries.forEach(entry => {
41      const { width } = entry.contentRect;
42      const cameraId = entry.target.dataset.cameraId;
43
44      let quality;
45      if (width < 200) quality = "low";
46      else if (width < 500) quality = "medium";
47      else quality = "high";
48
49      entry.target.dataset.quality = quality;
50      console.log(`Camera ${cameraId}: quality = ${quality} (width: ${width}px)`);
51    });
52  }
53
54  destroy() {
55    this.visibilityObserver.disconnect();
56    this.resizeObserver.disconnect();
57  }
58}

Summary

Dr. Grant sums up: "Observers are like our sensor system in the park — we don't have to manually check every camera all the time:"

  1. IntersectionObserver — reacts when an element enters or exits the visible area
  2. Lazy loading — load images/content only when the user scrolls to them
  3. Infinite scroll — automatically load more data as the user scrolls
  4. ResizeObserver — reacts to changes in DOM element dimensions
  5. Responsive components — adjust layout without media queries

Both APIs are asynchronous and far more efficient than manually listening to

scroll
or
resize
events. The browser optimizes callback invocations so they don't block the main thread.

Go to CodeWorlds