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.
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
requestIdleCallbackwhere 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
srcplaceholder (LQIP) for perceived speed. - Prioritize the hero image (do not lazy load it).
- Use
srcsetand responsive images for bandwidth savings. - For long lists, combine lazy-loading with virtualization to reduce DOM nodes.
Quick Practice
- Add
data-srcimages 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.
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