Chapter 8.7☕ 18 min read

IntersectionObserver, MutationObserver & ResizeObserver

Scroll event bhool jao, Observer lagao. Browser khud batayega — visible hua, DOM badla, size change hua.

01requestAnimationFrame — Browser Ki Native Render Clock

IntersectionObserver watches elements and tells you when they enter or leave the viewport (or a root element). It replaces scroll event listeners — much more performant because it runs after layout and uses GPU layer positions.

Creating an observer: new IntersectionObserver(callback, options) where options include:

root: The element used as the viewport for checking visibility (defaults to browser viewport / null).

rootMargin: Expand or shrink the observation area like CSS margin. For example, rootMargin: '200px' triggers 200px before the element enters the viewport.

threshold: 0 to 1 — the percentage of the target visible that triggers the callback. 0 = any pixel, 0.5 = 50%, 1 = fully visible.

The callback receives an array of IntersectionObserverEntry objects: isIntersecting, intersectionRatio, target, boundingClientRect, rootBounds.

Key methods: observer.observe(element) starts watching. observer.unobserve(element) stops watching one element. observer.disconnect() stops all observations.

Use cases: Lazy loading images, infinite scroll, reveal-on-scroll animations, ad impression tracking.

// Lazy loading images with IntersectionObserver
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src; // Load the real image
      img.classList.add('loaded');
      observer.unobserve(img); // Stop watching — loaded!
    }
  });
}, {
  root: null, // Use viewport
  rootMargin: '200px', // Start loading 200px BEFORE visible
  threshold: 0 // Trigger when even 1px is visible
});

// Observe all lazy images
document.querySelectorAll('img[data-src]').forEach(img => {
  observer.observe(img);
});

// Reveal-on-scroll animation
const revealObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      entry.target.classList.add('revealed');
    }
  });
}, { threshold: 0.2 }); // Trigger when 20% visible

document.querySelectorAll('.animate-on-scroll').forEach(el => {
  revealObserver.observe(el);
});
Why IntersectionObserver is fast: It is implemented using a COMPOSITE-LEVEL check. It runs AFTER layout and paint, using GPU layer positions. This is WHY it is more efficient than scroll event listeners — it does not force synchronous layout calculations. The browser does the math on its own schedule.
02RAF vs setTimeout — Kaun Kitna Smooth?

MutationObserver watches a DOM element for changes to its children, attributes, or text content. It is the DOM change detector — when anything in the subtree changes, you get notified.

Creating an observer: new MutationObserver(callback) then observer.observe(target, config).

Config options:

childList: Watch for child additions and removals.

attributes: Watch for attribute changes on the target.

characterData: Watch for text content changes.

subtree: Watch ALL descendants, not just direct children.

The callback receives an array of MutationRecord objects: type, target, addedNodes, removedNodes, attributeName, oldValue.

Important: Set attributeOldValue: true and characterDataOldValue: true if you need the old value in records.

// Watch for DOM changes
const target = document.getElementById('dynamic-content');

const observer = new MutationObserver((mutations) => {
  mutations.forEach(mutation => {
    if (mutation.type === 'childList') {
      console.log('Children changed!');
      console.log('Added:', mutation.addedNodes);
      console.log('Removed:', mutation.removedNodes);
    }
    if (mutation.type === 'attributes') {
      console.log('Attribute changed:', mutation.attributeName);
      console.log('Old value:', mutation.oldValue);
    }
    if (mutation.type === 'characterData') {
      console.log('Text changed!');
      console.log('Old text:', mutation.oldValue);
    }
  });
});

observer.observe(target, {
  childList: true,     // Watch for added/removed children
  attributes: true,    // Watch for attribute changes
  characterData: true, // Watch for text content changes
  subtree: true,       // Watch ALL descendants
  attributeOldValue: true,     // Include old attribute value
  characterDataOldValue: true  // Include old text value
});

// Stop watching
// observer.disconnect();
📋 Batching: MutationObserver callbacks are batched as microtasks. If you make 100 DOM changes synchronously, the observer fires ONE callback with 100 MutationRecords. This is efficient — but be careful with expensive processing in the callback.
03Layout Thrashing — Reads aur Writes Ka Ghotala

ResizeObserver watches an element's size — fires when the element's content rectangle changes. Different from window.resize: ResizeObserver watches INDIVIDUAL elements, not just the viewport.

Use case: Responsive components that adapt when their container resizes (not just the window). Perfect for widget-based architectures.

Callback receives an array of ResizeObserverEntry objects: target, contentRect (DOMRectReadOnly), borderBoxSize, contentBoxSize.

contentRect has: x, y, width, height, top, right, bottom, left.

Warning: ResizeObserver may cause layout thrashing if the callback resizes elements — use requestAnimationFrame to break the cycle.

// Responsive component that adapts to container size
const container = document.getElementById('widget');

const resizeObserver = new ResizeObserver((entries) => {
  for (const entry of entries) {
    const width = entry.contentRect.width;

    // Adapt layout based on container width
    if (width < 400) {
      container.classList.remove('layout-medium', 'layout-large');
      container.classList.add('layout-small');
    } else if (width < 800) {
      container.classList.remove('layout-small', 'layout-large');
      container.classList.add('layout-medium');
    } else {
      container.classList.remove('layout-small', 'layout-medium');
      container.classList.add('layout-large');
    }

    console.log('Container width:', width);
  }
});

resizeObserver.observe(container);

// ⚠️ BE CAREFUL: Don't resize the observed element in the callback!
// This can cause an infinite loop:
// resizeObserver observes container → callback makes container wider →
// ResizeObserver fires again → callback makes container wider → INFINITE!

// ✅ Safe: Use requestAnimationFrame for resize operations
const safeObserver = new ResizeObserver((entries) => {
  requestAnimationFrame(() => {
    // Resize operations here — runs once per frame, not per callback
    updateLayout(entries);
  });
});
04Virtual Scrolling — Hazaar Elements, Zero Lag

Clipboard API lets you read from and write to the system clipboard programmatically. navigator.clipboard.writeText(text) copies text (needs HTTPS + user gesture). navigator.clipboard.readText() reads text (needs permission).

Clipboard requires a Secure Context (HTTPS or localhost) AND transient user activation (click/keypress). Missing either = NotAllowedError.

For rich data: navigator.clipboard.write([new ClipboardItem({'text/plain': blob})]).

Page Visibility API tells you whether the page is visible to the user. document.visibilityState returns 'visible' or 'hidden'. Listen with document.addEventListener('visibilitychange', handler).

Use the Visibility API to: pause video/animations when tab is hidden, stop polling, reduce CPU usage.

document.hidden is a boolean shortcut — true when the tab is not visible.

// Clipboard API — copy on button click
copyBtn.addEventListener('click', async () => {
  try {
    await navigator.clipboard.writeText('Hello Hyderabad!');
    console.log('Copied!');
  } catch (err) {
    // Fallback for older browsers or non-HTTPS
    const textarea = document.createElement('textarea');
    textarea.value = 'Hello Hyderabad!';
    document.body.appendChild(textarea);
    textarea.select();
    document.execCommand('copy');
    document.body.removeChild(textarea);
  }
});

// Clipboard API — paste
pasteBtn.addEventListener('click', async () => {
  try {
    const text = await navigator.clipboard.readText();
    console.log('Pasted:', text);
  } catch (err) {
    console.log('Permission denied or not HTTPS');
  }
});

// Page Visibility — pause when tab hidden
document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'visible') {
    console.log('Tab visible — resume animations, polling');
    startPolling();
    resumeAnimations();
  } else {
    console.log('Tab hidden — pause to save CPU');
    stopPolling();
    pauseAnimations();
  }
});
05Performance Profiling — Browser Tools Ka Istemal

ALWAYS disconnect observers when they are no longer needed — prevents memory leaks. In SPAs: disconnect on component unmount, re-observe on mount.

IntersectionObserver: Use unobserve() after one-time actions (like lazy loading). Use disconnect() to stop all.

MutationObserver: Use disconnect() when you are done watching. It also stops all pending callbacks.

ResizeObserver: Be careful not to resize observed elements in the callback — causes infinite loop.

Observer pattern vs Event Listener: Observers are for specific monitoring tasks (visibility, mutations, size). Event listeners are for general interaction (click, keydown). Observers are much cheaper than polling with setInterval or scroll events.

// ✅ Proper cleanup pattern
class LazyLoader {
  constructor() {
    this.observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          this.loadImage(entry.target);
          this.observer.unobserve(entry.target);
        }
      });
    }, { rootMargin: '200px' });
  }

  init() {
    document.querySelectorAll('img[data-src]').forEach(img => {
      this.observer.observe(img);
    });
  }

  destroy() {
    this.observer.disconnect(); // ✅ Clean up! Prevents memory leaks!
  }
}

// Visibility API cleanup
const pollInterval = setInterval(fetchData, 5000);

document.addEventListener('visibilitychange', () => {
  if (document.hidden) {
    clearInterval(pollInterval); // Stop polling when hidden
  } else {
    setInterval(fetchData, 5000); // Resume when visible
  }
});

// Performance comparison:
// ❌ Scroll event listener (runs on EVERY scroll frame)
// window.addEventListener('scroll', checkVisibility); // 60fps × check = expensive

// ✅ IntersectionObserver (runs only when visibility CHANGES)
// const observer = new IntersectionObserver(callback); // Only on change = cheap
MutationObserver recursion guard: MutationObserver callbacks are batched as microtasks and run asynchronously. Multiple synchronous DOM changes produce ONE callback with multiple records. However, if a MutationObserver callback makes MORE DOM changes that trigger the same observer, you could create an infinite loop. V8 protects against this by limiting recursive MutationObserver calls.

Lo kar liya — Key Points:

  • ✅ IntersectionObserver watches elements for visibility changes — replaces expensive scroll event listeners for lazy loading and reveal animations
  • ✅ rootMargin expands the observation area (like CSS margin) and threshold sets the percentage visible that triggers the callback
  • ✅ MutationObserver watches DOM elements for child, attribute, or text changes — callbacks are batched as microtasks
  • ✅ Always set attributeOldValue and characterDataOldValue to true if you need the old value in MutationRecord
  • ✅ ResizeObserver watches individual element size changes — different from window.resize which only watches the viewport
  • ✅ Be careful not to resize observed elements in ResizeObserver callbacks — can cause infinite loops
  • ✅ Clipboard API requires HTTPS and user gesture — always provide a fallback for non-HTTPS contexts
  • ✅ Page Visibility API lets you pause expensive work when the tab is hidden — use visibilitychange event
  • ✅ Always disconnect observers when they're no longer needed to prevent memory leaks
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase