Unlocking Intersection Observer: Deep Dive, Performance Tests & Advanced Tricks

This comprehensive guide explores the Intersection Observer API, explaining its core concepts, detailed code examples, performance comparisons with scroll events, advanced usage like sticky positioning, visibility tracking, and browser compatibility, providing developers with practical insights to efficiently implement and optimize intersection-based interactions.

WecTeam
WecTeam
WecTeam
Unlocking Intersection Observer: Deep Dive, Performance Tests & Advanced Tricks
Original article: https://css-tricks.com/an-explanation-of-how-the-intersection-observer-watches/ Author: Travis Alman Translation: Liu Hui

Intersection Observer Overview

The W3C Working Draft (Sept 14, 2017) defines the Intersection Observer API as an interface for asynchronously observing the visibility and position of a target element relative to a root element or the viewport, useful for lazy‑loading and pre‑loading content.

The API provides a way to be notified when a child element enters the bounding box of one of its ancestors. Before Intersection Observer, this was typically done by listening to scroll events.

Although Intersection Observer offers a higher‑performance solution, it should be seen as complementary to scroll events rather than a complete replacement.

Basic Example

A minimal example demonstrates the four parts of an observer: root, target, options, and the callback function.

const options = {
  root: document.body,
  rootMargin: '0px',
  threshold: 0
};
function callback(entries, observer) {
  console.log(observer);
  entries.forEach(entry => {
    console.log(entry);
  });
}
let observer = new IntersectionObserver(callback, options);
observer.observe(targetElement);

The options object defines root (the scrolling container, defaulting to the viewport), rootMargin (extra margin around the root, similar to CSS margin), and threshold (an array of ratios that trigger the callback).

The callback receives two arguments: entries (an array of IntersectionObserverEntry objects) and the observer itself. Each entry provides useful properties such as isIntersecting, intersectionRatio, boundingClientRect, intersectionRect, rootBounds, target, and time.

Observer Object

When the observer is created, the console shows an object similar to:

IntersectionObserver {
  root: null,
  rootMargin: "0px 0px 0px 0px",
  thresholds: [0]
}

Entry Object

Each entry contains detailed geometry information. The most frequently used properties are isIntersecting (boolean) and intersectionRatio (a value between 0 and 1 indicating how much of the target is inside the root).

Observer Methods

observe(): start observing a target element.

unobserve(): stop observing a previously observed element.

disconnect(): stop observing all targets.

These methods cannot change the options after the observer is created; a new observer must be instantiated for different options.

Performance Comparison with Scroll Events

Three benchmark tests compare Intersection Observer against scroll‑event listeners on both a high‑end Mac and an average Windows PC. The results consistently show that Intersection Observer is significantly faster, especially when many observers are used.

Understanding intersectionRatio

The intersectionRatio represents the percentage of the target’s area that is intersecting the root. It is tied to the thresholds supplied in the options, but the value may not be exact due to rounding.

In a demo with 100 thresholds (0, 0.01, …, 1), moving the target left, top, or to a corner updates the ratio to roughly 0.5, 0.5, 0.25, and 1 respectively.

[...Array(100).keys()].map(x => x/100)

Finding Position

By inspecting boundingClientRect and intersectionRect you can determine whether the target is above, below, or fully inside the root.

const output = document.querySelector('#output pre');
function io_callback(entries) {
  const ratio = entries[0].intersectionRatio;
  const boundingRect = entries[0].boundingClientRect;
  const intersectionRect = entries[0].intersectionRect;
  if (ratio === 0) {
    output.innerText = 'outside';
  } else if (ratio < 1) {
    if (boundingRect.top < intersectionRect.top) {
      output.innerText = 'on the top';
    } else {
      output.innerText = 'on the bottom';
    }
  } else {
    output.innerText = 'inside';
  }
}

Creating Sticky‑Position Events

Using Intersection Observer with CSS position: sticky allows detection of when a sticky element reaches the top of its scrolling container. The trick is to set a negative rootMargin (e.g., 0px 0px -100% 0px) so that the observer fires as soon as the element touches the top edge.

<section>
  <div class="sticky-container">
    <div class="sticky-content">
      <span>§</span>
      <h2>Section 1</h2>
    </div>
  </div>
  {{ content here }}
</section>
.sticky-content {
  position: relative;
  transition: 0.25s;
}
.sticky-content span {
  display: inline-block;
  font-size: 20px;
  opacity: 0;
  overflow: hidden;
  transition: 0.25s;
  width: 0;
}
.sticky-content h2 { display: inline-block; }
.sticky-container { position: sticky; top: 0; }
.sticky-container.active .sticky-content {
  background-color: rgba(0,0,0,0.8);
  color: #fff;
  padding: 10px;
}
.sticky-container.active .sticky-content span {
  opacity: 1;
  transition: 0.25s 0.5s;
  width: 20px;
}
const stickyContainers = document.querySelectorAll('.sticky-container');
const io_options = {
  root: document.body,
  rootMargin: '0px 0px -100% 0px',
  threshold: 0
};
const io_observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    entry.target.classList.toggle('active', entry.isIntersecting);
  });
}, io_options);
stickyContainers.forEach(el => io_observer.observe(el));

Combining with Scroll Events

For scenarios that need higher precision than intersectionRatio provides, you can add a scroll listener only while the target is intersecting.

const root = document.querySelector('#root');
const target = document.querySelector('#target');
const output = document.querySelector('#output pre');
const io_options = { root, rootMargin: '0px', threshold: 0 };
let io_observer;
function scrollingEvents(e) {
  output.innerText = e.timeStamp;
}
function io_callback(entries) {
  if (entries[0].isIntersecting) {
    root.addEventListener('scroll', scrollingEvents);
  } else {
    root.removeEventListener('scroll', scrollingEvents);
    output.innerText = 0;
  }
}
io_observer = new IntersectionObserver(io_callback, io_options);
io_observer.observe(target);

Browser Support

Intersection Observer is supported by all major browsers except Internet Explorer, which can be polyfilled.

Intersection Observer v2 (Visibility Tracking)

New options trackVisibility (boolean) and delay (minimum 100 ms) enable the browser to report whether an element is truly visible to the user. The entry object gains an isVisible property.

const io_observer = new IntersectionObserver(callback, {
  root: null,
  rootMargin: '0px',
  threshold: 0,
  trackVisibility: true,
  delay: 100
});

These features are still experimental and may behave inconsistently across browsers.

Conclusion

The article provides a thorough exploration of the Intersection Observer API, from basic usage and code structure to performance testing, advanced patterns like sticky positioning and visibility tracking, and cross‑browser considerations, equipping developers with the knowledge to adopt this efficient API in real projects.

References

[1] demo: https://codepen.io/talmand/embed/VwZXpaj?height=632&theme-id=1&default-tab=result&user=talmand&slug-hash=VwZXpaj&pen-title=Intersection%20Observer%3A%20intersectionRatio&name=cp_embed_1 [2] demo2: https://codepen.io/talmand/embed/dybmvZN?height=631&theme-id=1&default-tab=result&user=talmand&slug-hash=dybmvZN&pen-title=Intersection%20Observer%3A%20Finding%20the%20Position&name=cp_embed_2 [3] CSS position sticky: https://css-tricks.com/almanac/properties/p/position/#article-header-id-3 [4] demo3: https://codepen.io/talmand/embed/ExYLayz?height=400&theme-id=1&default-tab=result&user=talmand&slug-hash=ExYLayz&pen-title=Intersection%20Observer%3A%20Position%20Sticky%20Event&name=cp_embed_3 [5] demo4: https://cdpn.io/talmand/fullembedgrid/wvwjBry?type=embed&animations=run [6] polyfill: https://github.com/w3c/IntersectionObserver/tree/master/polyfill [7] example1: https://cdpn.io/talmand/fullembedgrid/oNvdQOR?type=embed&animations=run [8] example2: https://cdpn.io/talmand/fullembedgrid/mdbLQZJ?type=embed&animations=run [9] animating clip‑path: https://css-tricks.com/animating-with-clip-path/ [10] intersectionRect algorithm: https://www.w3.org/TR/intersection-observer/#calculate-intersection-rect-algo [11] clip‑path property: https://www.w3.org/TR/css-masking-1/#propdef-clip-path [12] Google’s suggestions: https://developers.google.com/web/updates/2019/02/intersectionobserver-v2 [13] Updated proposal: https://szager-chromium.github.io/IntersectionObserver/ [14] This article: https://developers.google.com/web/updates/2019/02/intersectionobserver-v2 [15] Second‑type errors: https://szager-chromium.github.io/IntersectionObserver/#calculate-visibility-algo

JavaScriptIntersectionObserverWeb API
WecTeam
Written by

WecTeam

WecTeam (维C团) is the front‑end technology team of JD.com’s Jingxi business unit, focusing on front‑end engineering, web performance optimization, mini‑program and app development, serverless, multi‑platform reuse, and visual building.

0 followers
Reader feedback

How this landed with the community

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.