IntersectionObserver: Async Off-Main-Thread Threshold Checks
- Last updated
- Prerequisites:
- DOM and viewport concepts
- Understanding of scroll events and layout
- intersection-observer
- scroll
- performance
- lazy-loading
- observers
- viewport
Read at your depth
The practical view
IntersectionObserver reports when an element intersects the viewport (or another root element) — useful for lazy-loading images, infinite scroll, and read-progress indicators. const io = new IntersectionObserver((entries) => { entries.forEach(e => { if (e.isIntersecting) load(e.target); }); }, { threshold: 0.25 }); io.observe(img);. The callback runs asynchronously when the observed element crosses configured thresholds (0 to 1, or an array like [0, 0.5, 1]). Unobserve/disconnect when done. Compared to scroll + getBoundingClientRect, this runs off the hot scroll path and is far cheaper.
The same idea in other frameworks
react-intersection-observer wraps the API for hooks; React apps use it for lazy images and infinite lists.Angular can use the API in directives or the browser's native lazy loading; @angular/cdk has a ScrollDispatcher but IO is preferred for visibility.vueuse's useIntersectionObserver integrates it with component lifecycle; Vue lazy-image components commonly wrap it.Svelte actions (use:action) are a natural fit for attaching an IntersectionObserver to an element on mount.Legacy vs modern
Scroll + getBoundingClientRect vs IntersectionObserver for visibility
Reading geometry on every scroll forces synchronous layout and janks; IntersectionObserver computes intersection asynchronously and fires only on threshold crossings.
window.addEventListener('scroll', () => { const r = el.getBoundingClientRect(); if (r.top < innerHeight) load(); }); // ❌ OLD: layout read per scroll frameconst io = new IntersectionObserver(entries => { if (entries[0].isIntersecting) load(); });
io.observe(el); // ✅ NEW: async, engine-computed, threshold-basedInterview gotchas
Context
This is the classic performance question — interviewers want the layout-thrash and off-main-thread mechanics, not just 'it's the modern API'.
The mechanical answer
A scroll listener runs on the main thread at scroll frequency; every getBoundingClientRect call inside forces a synchronous layout pass to return up-to-date geometry, which can invalidate and re-flow the page mid-frame — the leading cause of scroll jank. IntersectionObserver moves the intersection computation into the engine's own asynchronous rendering/compositor pipeline: after a scroll or layout update, the engine checks observed targets against the root box and only queues a callback when a threshold is crossed. This avoids per-scroll layout reads, is coalesced, and scales to many observed elements without binding main-thread work to scroll frequency.
Trap
The naive answer is 'getBoundingClientRect is accurate but IntersectionObserver is approximate'. Both are accurate; the difference is synchronous-layout cost vs async engine computation. Another trap: assuming the IO callback fires on every scroll position change — it fires only when the intersection ratio crosses a configured threshold, so code that expects per-frame updates must use appropriate thresholds or fall back to rAF-driven measurement for pixel-exact animations.
Context
This tests knowledge of delivery timing (a task, not a microtask) and the threshold/rootMargin tuning knobs that control when callbacks fire.
The mechanical answer
The callback is queued as a task on the observer's task source after the engine's intersection computation pass (part of the rendering steps), so it runs asynchronously after the current task — not in the middle of your synchronous code, and not as a microtask. The intersection ratio is compared against the thresholds array; when a value newly crosses any threshold (including 0 and 1), an entry is queued. rootMargin expands or shrinks the root's intersection rectangle before clipping, letting you trigger callbacks before the element actually enters the viewport (pre-loading). This timing and threshold behavior is what makes it safe for lazy-loading without blocking rendering.
Trap
The naive answer is 'the callback fires synchronously during scroll'. It is an async task delivered after the engine's intersection pass. Another trap: assuming isIntersecting equals 'fully visible' — it only means the intersection ratio exceeds the current threshold (e.g. threshold: 0 fires as soon as 1px is visible). Candidates who explain the task-source delivery and ratio/threshold crossing mechanics show real depth.