Loading every image on a long page up front wastes bandwidth on pictures the user may never scroll to. The fix is lazy loading: ship the markup with the real URL parked in data-src and an empty src, then swap it in only when the image is about to enter the viewport. IntersectionObserver makes this cheap — it watches elements against the viewport and calls you when they cross in, no scroll-handler math required.
Implement lazyLoadImages(root, options). Observe each img[data-src] under root, and on intersection move data-src → src, then stop watching it. See MDN: lazy loading.
function lazyLoadImages(root, options) {
// returns the IntersectionObserver (or null when falling back)
}
<img data-src="hero.jpg" /> <!-- no src yet: nothing downloads -->
lazyLoadImages(document.body, { rootMargin: '200px' });
// as the <img> nears the viewport (200px early):
// img.src = 'hero.jpg'; the browser fetches it; data-src is removed;
// the observer stops watching that image.
img[data-src] — leave images that already have a src alone.rootMargin preloads early — a positive margin (e.g. '200px') starts the fetch just before the image scrolls into view, avoiding a visible pop-in.IntersectionObserver isn't available, load every image immediately so nothing stays blank.Unlock the solution & editor
Runnable editor + tests
Solve in the browser with instant Jest feedback.
Detailed solutions
Walkthroughs, edge cases, and complexity notes.
Multi-framework variants
React, Vue, Vanilla, Angular — same question, different stacks.