Theme Framework How the LeaseLeads theme frontend works

Observers

Singleton, multiplexed wrappers over IntersectionObserver and ResizeObserver — one real observer per options signature, shared across every consumer.

On this page 3

Native IntersectionObserver and ResizeObserver are cheap to create and expensive to have hundreds of. Every component that wants to fade in on scroll, lazy-load an image, or react to a resize would otherwise mint its own observer, and each one is a fresh source of layout reads. The theme wraps both in singletons that multiplex — one real observer shared by every consumer — so you get resource conservation and a single, coordinated reflow source instead of N competing ones.

The API is the same shape for both: construct with an element and a callback, then call observe().

Intersection — one observer per options signature

GlobalIntersectionObserver keeps a static registry keyed by the options signature. Consumers that ask for the same threshold/rootMargin share one underlying IntersectionObserver; a different signature lazily mints a new group. Within a group, an element → Set<consumer> registry lets many consumers watch the same element, and each entry fans out to every consumer registered for its target.

js
// components/Animation/Animation.js
this.observer = new GlobalIntersectionObserver(this.element, this.animateIn.bind(this))
// …later, once ready:
this.observer.observe()

Pass options as the third argument to join (or create) the matching group. The default is a 0.33 threshold:

js
// modules/LocationMap/LocationMap.js — a 300px pre-load margin
;(new GlobalIntersectionObserver(this.element, () => {
  // boot the map just before it scrolls into view
}, { rootMargin: '300px 0px' })).observe()

Intersection is one-shot: when an element intersects, the observer unobserves it before firing your callback, so a fade-in or a lazy-load runs exactly once — you don't have to disconnect anything yourself. That makes it the right tool for lazy-loading an image the moment it nears the viewport:

js
// components/Image/Image.js
;(new GlobalIntersectionObserver(this.element, () => {
  // swap in the real source
})).observe()

Resize — one observer, with delta tracking

GlobalResizeObserver shares a single ResizeObserver across every consumer via the same element → Set<consumer> registry. Each consumer tracks its own previous width and height, so its callback receives not just the current size but the delta since the last call — letting you react only to the axis that actually changed:

js
// components/Masonry/Masonry.js
this.#_observer = new GlobalResizeObserver(this.element, ({ inline }) => {
  if ( inline !== false ) {
    this.refresh()   // only relayout when the width moved
  }
})

The callback is invoked with { inline, block, width, height } — where inline and block are the signed deltas (or false when that axis didn't move). Two details worth knowing:

  • Callbacks run on a background task. Every fan-out is scheduled through backgroundTask, so resize reactions never pile onto the frame that fired them. See Scheduling.
  • One initial measurement is forced. The constructor schedules a first #call() so a consumer that registers late still gets its starting dimensions.

It also attaches a single debounced window.resize listener that re-fires every consumer when the viewport height changes — the fix for mobile URL-bar resizes that a ResizeObserver alone misses.

Note

Why singletons. The point isn't only fewer objects. A ResizeObserver callback is a layout read; funnelling every consumer through one observer and one background task makes the whole page's resize handling a single, schedulable reflow source instead of a scattered pile of them. The registry is a Set per element precisely so adding an Nth consumer costs nothing at the browser level.

  • SchedulingbackgroundTask, which every Resize callback is deferred through.
  • Sub-components & HasX — the hosts that construct observers as they wire behaviour.