Implement Lazy Image Loading Without IntersectionObserver

Build a lightweight lazy-loading system that defers image downloads until they’re likely to be visible — without IntersectionObserver.

intermediateBrowser and domjavascriptlazy-loadingimagesperformance
Published: 11/12/2025
Updated: 11/12/2025

Advertisement

Question

Implement lazy loading for images without using IntersectionObserver. The solution should:

  • Avoid blocking the main thread
  • Use a small memory footprint
  • Gracefully load images as they approach the viewport

Answer

Lazy loading defers image downloads until they’re needed, which improves initial page load, decreases bandwidth usage, and increases perceived performance. In 2025, loading="lazy" and IntersectionObserver are common but you may need a fallback for older browsers or custom behaviors (preload when near viewport, low-quality placeholders, prioritized images).

A robust non-IntersectionObserver approach uses:

  • A lightweight scroll/resize/throttle combo
  • Bounding-box checks (getBoundingClientRect)
  • A small prefetch margin to load images before they enter viewport
  • requestIdleCallback where available

Why these choices help SEO & UX:

  • Faster LCP (Largest Contentful Paint) when above-the-fold images are prioritized
  • Lower bandwidth for mobile users
  • Search engines appreciate pages that load quickly and progressively

Implementation

function throttle(fn, wait = 100) {
  let last = 0;
  return function (...args) {
    const now = Date.now();
    if (now - last >= wait) {
      last = now;
      fn.apply(this, args);
    }
  };
}

function isNearViewport(el, offset = 300) {
  const rect = el.getBoundingClientRect();
  return (
    rect.top <
    (window.innerHeight || document.documentElement.clientHeight) + offset
  );
}

function lazyLoadImages() {
  const imgs = document.querySelectorAll('img[data-src]');
  imgs.forEach((img) => {
    if (isNearViewport(img)) {
      img.src = img.dataset.src;
      img.removeAttribute('data-src');
      img.classList.add('lazy--loaded');
    }
  });
}

const handler = throttle(() => {
  if ('requestIdleCallback' in window) {
    requestIdleCallback(lazyLoadImages);
  } else {
    lazyLoadImages();
  }
}, 200);

window.addEventListener('scroll', handler, { passive: true });
window.addEventListener('resize', handler);
document.addEventListener('DOMContentLoaded', handler);

HTML markup example:

<img
  data-src="/images/photo.jpg"
  alt="..."
  src="/images/placeholder-small.jpg"
  class="lazy" />

Progressive enhancement:

  • Use loading="lazy" attribute when supported to benefit native lazy loading.
  • For LCP-critical images (hero banners), load immediately and not lazily.

Visualization

sequenceDiagram participant User participant Browser participant Script User->>Browser: scroll Browser->>Script: throttled event Script->>Script: check images near viewport Script-->>Browser: set img.src (starts download)

Real-World Example & Tips

  • Use a tiny inline SVG or blurred tiny image as src placeholder (LQIP) for perceived speed.
  • Prioritize the hero image (do not lazy load it).
  • Use srcset and responsive images for bandwidth savings.
  • For long lists, combine lazy-loading with virtualization to reduce DOM nodes.

Quick Practice

  • Add data-src images to a long page and test:
    • Scroll slowly — images should load just before they appear.
    • Disable JS and verify the page still loads (progressive enhancement).

Summary

A scroll+throttle + bounding-box fallback is reliable for older browsers. Use requestIdleCallback where available and prefer IntersectionObserver for modern browsers. Prioritize LCP images and use placeholders for better UX.

Frequently Asked Questions

Is IntersectionObserver always the best choice?

IntersectionObserver is ideal when available, but graceful fallbacks that use scroll/resize handlers are necessary for older browsers.

Advertisement


Stay Updated

Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.

Advertisement