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 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 browserIntersectionObserver 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 observationsThe 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]");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}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);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}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}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:"
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.