Stockio

Intersection Observer Lazy Loading and Scroll Effects

Use IntersectionObserver to load optional content and trigger one-time reveals without scroll polling. Configure roots, margins, thresholds and cleanup safely.

By Daniel Kim

React Developer ·

On this page

IntersectionObserver lets the browser notify your code when an element crosses a visibility boundary. Instead of measuring every card during every scroll event, you declare what you want observed and react to asynchronous entries. The API suits deferred widgets, section highlighting and one-time reveals, provided you understand what intersection actually measures.

Decide what should wait for intersection

Start with content that can arrive later without blocking the main task. A map near the footer is a better candidate than the hero image. For deferred images, use the image resizer calculator to establish dimensions before writing observer code. Reserving the correct aspect ratio prevents loading behaviour from moving neighbouring content.

Prefer native lazy loading for ordinary offscreen images and iframes. It needs less code and lets the browser choose loading distance. IntersectionObserver becomes useful when you need custom application work: importing a chart module, preparing a preview or starting a reveal after a section enters a specific container.

Do not defer the likely largest above-the-fold image. Making that resource wait for JavaScript and an observer callback can hurt loading performance. Keep sizing and delivery decisions consistent with your responsive image pipeline, rather than treating observation as a substitute for appropriate assets.

Write down the trigger and completion conditions first. Does a target load once, restart on every entry, or remain active while visible? These are different state machines. A single boolean called visible usually hides important distinctions between eligibility, work in progress, success and failure.

Understand roots, margins and thresholds

The root is the rectangle against which intersection is evaluated. With root set to null, observation uses the viewport. For a scrolling sidebar, pass the scrolling ancestor element instead. Targets must belong to the relevant descendant hierarchy, so confirm the DOM relationship before debugging apparently missing callbacks.

Root margin expands or contracts that rectangle. A positive bottom margin starts work before content reaches the viewport; a negative margin delays the trigger. Start with a few hundred pixels for optional content, then test slow networks and quick scrolling. The right distance depends on resource cost and user behaviour, not a universal magic number.

A threshold is a proportion of the target's area intersecting the root, not a proportion of the viewport filled. A threshold of 0.5 can never be reached by some very tall targets in a short viewport. For basic deferred work, use threshold zero and check isIntersecting. Use several thresholds only when you need their transitions.

Callbacks can include several entries and may run after other main-thread tasks. They are not frame-perfect animation updates. Read the MDN Intersection Observer guide for the geometry model, including clipping and initial notifications. Ordinary intersection does not prove that another element is not covering the target.

Build a one-time loading lifecycle

The following pattern loads a custom preview exactly once after it approaches the viewport. It assumes each target has a data-preview-url attribute and contains an image with reserved dimensions. A failed request exposes a retry control instead of silently leaving an empty panel.

const targets = document.querySelectorAll("[data-preview-url]");

async function loadPreview(target) {
const image = target.querySelector("img");
if (!image) return;
target.dataset.state = "loading";
image.src = target.dataset.previewUrl;
try {
  await image.decode();
  target.dataset.state = "ready";
} catch {
  target.dataset.state = "error";
  const retry = target.querySelector("button");
  if (retry) retry.hidden = false;
}
}

const observer = "IntersectionObserver" in window
? new IntersectionObserver((entries) => {
    for (const entry of entries) {
      if (!entry.isIntersecting) continue;
      observer.unobserve(entry.target);
      void loadPreview(entry.target);
    }
  }, { rootMargin: "0px 0px 300px 0px", threshold: 0 })
: null;

for (const target of targets) {
if (observer) observer.observe(target);
else void loadPreview(target);
target.querySelector("button")?.addEventListener("click", () => {
  void loadPreview(target);
});
}

// Call when the owning view is removed.
function disposePreviews() {
observer?.disconnect();
}

Calling unobserve before asynchronous work prevents repeated entries from starting duplicate requests. This example retries through an explicit button; another valid policy is to re-observe after a controlled backoff. Avoid immediate automatic retries while a broken resource remains visible.

Disconnecting stops observation, not pending downloads or every event listener attached elsewhere. A component implementation should also remove listeners and cancel cancellable application requests during teardown. If you fetch data before rendering a chart, use AbortController and ignore stale results after navigation. See the MDN disconnect reference for its precise scope.

Trigger effects without hiding essential content

For scroll-triggered effects, keep meaningful content visible by default. Add an enhancement class only after observation is available and motion preferences have been checked. Otherwise a script failure can leave important paragraphs permanently transparent. A decorative reveal should never become a requirement for reading the page.

Prefer one-time transitions that add a class and then stop observing. Repeated entry and exit effects can flicker around a boundary, particularly when the transformation itself changes geometry. Observe a stable wrapper and animate a child when necessary. Keep the observer callback short; it should schedule or mark work, not synchronously build a large interactive scene.

Respect reduced motion by skipping nonessential movement when prefers-reduced-motion requests it. The content can appear immediately while retaining all functionality. The MDN media feature reference explains the preference. More elaborate timing belongs in a dedicated animation system, not a dense list of observer thresholds.

If you want continuous progress tied to scrolling, IntersectionObserver is usually the wrong abstraction. Threshold notifications are discrete and asynchronous. Use a suitable scroll-driven animation mechanism or another carefully designed approach instead of assuming the callback will provide a reliable value for every rendered frame.

Test boundaries, cleanup and fallback behaviour

Test inside both the document viewport and nested scrolling containers. Resize the window, open an accordion above the target and navigate back to the page. Geometry changes can produce observations without a wheel event. Your implementation should respond to the entries, not assumptions about which user gesture caused them.

Use shared observers for targets with identical configuration. Creating one observer per card makes lifecycle management harder without usually helping. After a target is complete, release it. When a view disappears, disconnect its observer and ensure asynchronous completions cannot mutate a replacement view.

Check fast scrolling on a throttled connection. If placeholders remain empty after they enter view, increase lead time or reduce the work required. Pair those tests with JavaScript payload reduction; postponing a huge module is not the same as making it inexpensive.

Key takeaways

  • Prefer native image loading unless custom lifecycle control is necessary.
  • Configure root geometry intentionally and treat thresholds as target-area ratios.
  • Unobserve one-time targets before asynchronous work begins.
  • Keep content available without reveal scripts or motion effects.
  • Test failures, nested scrollers and teardown as carefully as successful entry.
Featured sponsor

Reach designers & developers at work

Put your product next to the tools people use every day. One sponsor per placement, no tracking pixels.

Become a sponsor

Keep reading

Related articles

More in JavaScript & Frameworks
Performance & SEO

10 Best Image Optimization Tools

Compare ten image optimisation tools for shrinking photos, preparing web graphics, and automating delivery, with practical notes on format support, workflow fit, and quality trade-offs.

Includes: Image resizerMarcus BellMarcus Bell
Icons & Illustration

10 Best Character and Avatar Generators

Compare ten avatar and character makers for profile photos, games, presentations, and product mock-ups, with practical notes on customisation, export options, and choosing a style.

Includes: Image resizerMateo FernándezMateo Fernández
Branding & Business

10 Best Brand Mockup Sites for Presenting Your Work

Compare ten mockup sites for placing a logo or brand design on packaging, screens, and everyday objects, plus practical guidance for choosing realistic scenes and sharing polished previews.

Includes: Image resizerArjun RamanArjun Raman
Branding & Business

10 Best Social Media Scheduler Apps for Creators

Compare ten social media schedulers for solo creators and small teams, from visual planning to analytics, and learn which features matter before you move your posting routine.

Includes: Image resizerPiotr KowalskiPiotr Kowalski
Icons & Illustration

8 Best Apps for Digital Illustration

Find an illustration app that fits your device, budget, and drawing style, from beginner-friendly tablet programs to professional painting and vector tools, with honest trade-offs.

Includes: Image resizerAiko TanakaAiko Tanaka
Smartphones & Tablets

6 Best Tablets for Every Budget

Compare six established tablet families for reading, streaming, school, drawing, and work, with practical advice on accessories, software, storage, and value.

Includes: Image resizerDaniel KimDaniel Kim