Frontend Development 16 min read

Understanding Intersection Observer API and Its Polyfill Implementation

The article explains how the Intersection Observer API offloads visibility detection from scroll events to improve performance, details its creation, options, callback structure, and entry data, and describes a polyfill that mimics native behavior using event listeners, mutation observers, and geometric calculations for broader browser support.

DaTaobao Tech
DaTaobao Tech
DaTaobao Tech
Understanding Intersection Observer API and Its Polyfill Implementation

This article introduces the Intersection Observer API, explains why it solves common scrolling scenarios such as lazy‑loading images, infinite scrolling, and visibility‑based logic, and describes the underlying polyfill implementation.

Background

Traditional approaches listen to the scroll event and call getBoundingClientRect() on target elements to calculate their position relative to the viewport. This synchronous handling can be costly and often requires throttling.

The Intersection Observer API, defined by the W3C, moves the observation work off the main thread, reducing resource consumption and improving page performance.

API Overview

Creating an observer:

const observer = new IntersectionObserver(callback[, options]);

Key methods:

observer.observe(target)
observer.unobserve(target)
observer.disconnect()
observer.takeRecords()

Options

root : the ancestor element used as the viewport (default is the document viewport).

rootMargin : margin around the root, similar to CSS margin (default "0px 0px 0px 0px").

threshold : a number or array of numbers indicating at which visibility percentages the callback should fire (default 0).

Callback signature

function callback(entries, observer) { /* ... */ }

Each IntersectionObserverEntry provides:

time : high‑resolution timestamp.

target : the observed element.

rootBounds , boundingClientRect , intersectionRect .

isIntersecting and intersectionRatio .

Implementation Details (Polyfill)

The polyfill reproduces the native behavior by maintaining an internal list this._observationTargets of observed elements.

IntersectionObserver.prototype.observe = function(target) {
var isTargetAlreadyObserved = this._observationTargets.some(function(item) {
return item.element == target;
});
if (isTargetAlreadyObserved) { return; }
if (!(target && target.nodeType == 1)) { throw new Error('target must be an Element'); }
this._registerInstance();
this._observationTargets.push({element: target, entry: null});
this._monitorIntersections();
this._checkForIntersections();
}

Registration ensures the observer instance is stored in a global registry to avoid garbage collection:

IntersectionObserver.prototype._registerInstance = function() {
if (registry.indexOf(this) < 0) {
registry.push(this);
}
};

Intersection checking can be driven by either a polling interval or by native events (resize, scroll) and a MutationObserver for DOM changes:

IntersectionObserver.prototype._monitorIntersections = function() {
if (this.POLL_INTERVAL) {
this._monitoringInterval = setInterval(this._checkForIntersections, this.POLL_INTERVAL);
} else {
addEvent(window, 'resize', this._checkForIntersections, true);
addEvent(document, 'scroll', this._checkForIntersections, true);
if (this.USE_MUTATION_OBSERVER && 'MutationObserver' in window) {
this._domObserver = new MutationObserver(this._checkForIntersections);
this._domObserver.observe(document, {attributes:true, childList:true, characterData:true, subtree:true});
}
}
};

The core intersection calculation walks up the DOM tree, clipping the target rectangle against each ancestor that has a non‑visible overflow. The helper computeRectIntersection returns the intersecting rectangle:

function computeRectIntersection(rect1, rect2) {
var top = Math.max(rect1.top, rect2.top);
var bottom = Math.min(rect1.bottom, rect2.bottom);
var left = Math.max(rect1.left, rect2.left);
var right = Math.min(rect1.right, rect2.right);
var width = right - left;
var height = bottom - top;
return (width >= 0 && height >= 0) && {top, bottom, left, right, width, height};
}

Each time an intersection change is detected, a new IntersectionObserverEntry is created and queued. When the queue is non‑empty, the observer’s callback is invoked:

IntersectionObserver.prototype._checkForIntersections = function() {
// ... compute entries ...
if (this._queuedEntries.length) {
this._callback(this.takeRecords(), this);
}
};

The takeRecords method returns and clears the queued entries:

IntersectionObserver.prototype.takeRecords = function() {
var records = this._queuedEntries.slice();
this._queuedEntries = [];
return records;
};

Browser Compatibility

Before using the API, developers should check for native support and load the polyfill when necessary. The article links to the official MDN documentation and the polyfill source on GitHub.

Conclusion

Intersection Observer provides an efficient, asynchronous way to monitor element visibility, eliminating heavy scroll‑event handling. The polyfill reproduces this behavior with a combination of event listeners, mutation observers, and geometric calculations, enabling broader browser support.

PerformanceJavaScriptIntersectionObserverlazy-loadingpolyfillWeb API
DaTaobao Tech
Written by

DaTaobao Tech

Official account of DaTaobao Technology

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.