Back
Web Fundamentals

Import on Visibility: Lazy Loading with IntersectionObserver

On this page

mediumRendering & Browser Architecture

Import on Visibility: Lazy Loading with IntersectionObserver

TL;DRUse IntersectionObserver + dynamic import() to load heavy components only when they enter the viewport
High Signal
Google
Meta
Agoda
Meesho
30-Second Answerstart every interview with this

Import on Visibility combines IntersectionObserver with dynamic import() to load JavaScript-heavy components only when they approach the user's viewport. This pattern further reduces initial bundle size for below-the-fold content while providing smooth perceived performance through rootMargin prefetching.

Static imports = cooking everything before opening the restaurant. Import on Interaction = cooking after they order. Import on Visibility = starting prep when you see them walking toward the table (via IntersectionObserver). You save resources on startup and still deliver smoothly when they arrive.

Placeholder renders (lightweight)
IntersectionObserver detects viewport approach
dynamic import() + rootMargin prefetch
Component mounts with loading UI

1IntersectionObserver Basics

The modern, efficient way to detect when an element enters the viewport without expensive scroll listeners. Use rootMargin to start loading slightly before the element is visible.

observer.jsjs
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      loadHeavyComponent();
      observer.disconnect();
    }
  });
}, { rootMargin: '150px' });

observer.observe(placeholderElement);

2Basic Implementation

Combine observer with dynamic import() to load and mount the real component only when needed.

3React Hook Pattern

Encapsulate logic into a reusable hook for clean, production-ready usage with proper cleanup and state management.

useLoadOnVisibility.jsjs
function useLoadOnVisibility(loader, rootMargin = '150px') {
  const ref = useRef(null);
  const [Component, setComponent] = useState(null);
  // ... loading & error states
}

4Reusable Component & Advanced Tips

Create <LazyLoadOnVisible /> wrapper. Support parallel imports, meaningful placeholders, error handling, and cleanup to prevent memory leaks.

PropertyImport on Mount (useEffect)Import on InteractionImport on Visibility
BehaviorLoads as soon as component mountsLoads on click/hover/focusLoads when near viewport
Use CaseContent needed soon after renderUser-triggered features (modals, editors)Below-the-fold heavy widgets
PerformanceEarlier but less optimalBest for explicit intentBalances delay with smoothness
Bundle ImpactStill affects initial load if above foldVery small initial bundleSmall initial bundle

Import on Mount (useEffect)

Behavior

Loads as soon as component mounts

Use Case

Content needed soon after render

Performance

Earlier but less optimal

Bundle Impact

Still affects initial load if above fold

Import on Interaction

Behavior

Loads on click/hover/focus

Use Case

User-triggered features (modals, editors)

Performance

Best for explicit intent

Bundle Impact

Very small initial bundle

Import on Visibility

Behavior

Loads when near viewport

Use Case

Below-the-fold heavy widgets

Performance

Balances delay with smoothness

Bundle Impact

Small initial bundle

Common questions

  • ›“How would you lazy load a component only when it scrolls into view?”
  • ›“Compare import on interaction vs import on visibility.”
  • ›“How do you implement this pattern in React?”
  • ›“What are the trade-offs and best practices?”

What interviewers look for

  • Correct use of IntersectionObserver + dynamic import()
  • Understanding of rootMargin for prefetching
  • Proper cleanup and single-load guarantees
  • Awareness of good vs bad candidates and UX considerations

Short answer (60 sec)

Use IntersectionObserver to watch a placeholder. When it enters the viewport (with rootMargin), trigger dynamic import() to load the heavy component. This keeps the initial bundle small while loading below-the-fold content just in time.

Detailed answer (senior level)

This pattern is ideal for comments, embeds, charts, and other below-the-fold heavy UI. rootMargin (e.g. 150px) starts loading early for smoother experience. Always disconnect the observer after loading, cache the module, and provide meaningful placeholders + error states. It complements import on interaction and React.lazy for comprehensive lazy loading strategy.

  • Forgetting to disconnect the observer (memory leaks)
  • Using scroll listeners instead of IntersectionObserver
  • Loading critical/above-the-fold content this way
  • No loading states or poor placeholders
  • Not handling errors or multiple triggers
Key Takeaways
  • ✓IntersectionObserver + dynamic import() = efficient below-the-fold lazy loading
  • ✓Use positive rootMargin to prefetch before visibility
  • ✓Always disconnect observer and guard against duplicate loads
  • ✓Best for comments, embeds, charts, recommendations — not core UI
  • ✓Provide excellent placeholders and error handling for great UX
  • ✓Combine with import on interaction for complete lazy loading coverage

Related web fundamentals topics

Continue with closely related browser and frontend engineering concepts.

Import on Interaction: Load When User InteractsBrowser Rendering Pipeline & Layout ThrashingDynamic Module Loading: import() FunctionRendering Strategies: CSR vs SSR vs SSG vs ISRJavaScript Module Systems: CJS vs ESM vs UMDStreaming SSR: Progressive HTML Streaming
Previous TopicImport on Interaction: Load When User InteractsNext Topic Browser Rendering Pipeline & Layout Thrashing