civil-and-structural-engineering
Implementing Lazy Loading in Javascript for Faster Web Pages
Table of Contents
Lazy loading is a performance optimization technique that defers the loading of non-critical resources until they are needed. Instead of loading every image, video, or iframe when a page first loads, lazy loading waits until the user scrolls near that element before fetching it. This approach dramatically reduces initial page weight, speeds up time-to-interactive, and conserves bandwidth — especially important for mobile users and content-heavy pages. Modern JavaScript, particularly the Intersection Observer API, makes implementing lazy loading both efficient and straightforward.
What Is Lazy Loading?
At its core, lazy loading is a design pattern that postpones the initialization of an object or resource until the point at which it is actually required. In web development, this typically means delaying the loading of images, videos, iframes, and other embedded content until they enter or are about to enter the browser’s viewport. The browser initially loads only the visible content (above the fold), then loads additional resources on demand as the user scrolls.
Native browser support for lazy loading exists via the loading="lazy" attribute on <img> and <iframe> tags. However, relying solely on native lazy loading can be inconsistent across browsers and offers less control over the loading behavior. JavaScript-based lazy loading gives developers fine-grained control, fallback handling, and the ability to lazy-load custom elements like background images or dynamically inserted content.
Why Use Lazy Loading?
The benefits of lazy loading go far beyond just perceived performance. Here are the key reasons to implement it:
- Faster initial load times — The browser downloads fewer bytes upfront, so the page becomes interactive sooner.
- Reduced bandwidth consumption — Images and media that the user never scrolls to are never fetched, saving data for visitors on metered connections.
- Improved Core Web Vitals — Lazy loading directly improves Largest Contentful Paint (LCP) by deferring offscreen images and helps keep Cumulative Layout Shift (CLS) low by reserving space for lazy-loaded elements.
- Lower server load — Fewer simultaneous requests mean less strain on your hosting infrastructure.
- Better performance on mobile devices — Mobile devices often have slower CPUs and network connections; lazy loading reduces the processing power needed at initial render.
Implementing Lazy Loading with the Intersection Observer API
The Intersection Observer API provides a performant way to detect when an element enters the viewport. Unlike scroll event listeners (which run on the main thread and can cause jank), IntersectionObserver operates asynchronously and is optimized by the browser. This makes it the recommended approach for lazy loading in modern JavaScript.
Step 1: Prepare Your HTML
Instead of putting the real image source in the src attribute, place it in a custom data attribute — commonly data-src. Optionally, you can provide a lightweight placeholder (such as a small blurred image or a base64 encoded low-quality image placeholder) in the actual src attribute to preserve layout space.
<img class="lazy" data-src="https://example.com/high-res-image.jpg" src="placeholder.jpg" alt="Description of the image" width="800" height="600" />
Always include explicit width and height attributes. This reserves the correct amount of space in the layout, preventing content from shifting when the image eventually loads.
Step 2: Write the Intersection Observer Code
Create an IntersectionObserver instance that watches all images with the class lazy. When an image intersects the viewport (or comes within a predefined root margin), swap the data-src into the src attribute, remove the lazy class, and stop observing that element.
document.addEventListener('DOMContentLoaded', function () {
const lazyImages = document.querySelectorAll('img.lazy');
if ('IntersectionObserver' in window) {
const imageObserver = new IntersectionObserver(function (entries, observer) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.remove('lazy');
observer.unobserve(img);
}
});
}, {
rootMargin: '100px 0px' // Load images 100px before they enter the viewport
});
lazyImages.forEach(function (img) {
imageObserver.observe(img);
});
} else {
// Fallback
}
});
A rootMargin of '100px 0px' triggers loading slightly before the element becomes visible, improving the perceived loading experience. Adjust this value based on your site’s typical scrolling speed and content density.
Step 3: Provide a Fallback for Older Browsers
While all modern browsers support IntersectionObserver, you should still support older environments (such as Internet Explorer 11 or older mobile browsers). A simple fallback is to load all images immediately if the API is not available:
if (!('IntersectionObserver' in window)) {
lazyImages.forEach(function (img) {
img.src = img.dataset.src;
img.classList.remove('lazy');
});
}
For more robust fallback handling, consider using a polyfill like the official IntersectionObserver polyfill or a lightweight lazy-loading library that falls back to scroll-event-based detection.
Advanced Lazy Loading Techniques
Beyond standard <img> tags, lazy loading can be applied to many other resource types. The same IntersectionObserver pattern applies, but the implementation details differ slightly.
Lazy Loading Images with srcset and Picture
Responsive images using srcset and sizes attributes are crucial for serving appropriately sized images to different devices. When lazy loading responsive images, swap the data-srcset and data-sizes attributes instead of data-src. Similarly, for <picture> elements, you may need to replace data-srcset on each <source> tag.
<img class="lazy"
data-src="image-800.jpg"
data-srcset="image-400.jpg 400w, image-800.jpg 800w, image-1200.jpg 1200w"
data-sizes="(max-width: 600px) 100vw, 50vw"
src="placeholder.jpg"
alt="Responsive lazy image" />
In your observer callback, you would set img.srcset = img.dataset.srcset and img.sizes = img.dataset.sizes along with img.src = img.dataset.src.
Lazy Loading Iframes and Videos
Iframes (e.g., embedded YouTube videos, maps, social media widgets) are heavy and often slow down pages. Apply the same data-attribute pattern: replace src with data-src and swap it when the iframe enters the viewport.
<iframe class="lazy-iframe" data-src="https://www.youtube.com/embed/example" width="560" height="315" frameborder="0" allowfullscreen></iframe>
const lazyIframes = document.querySelectorAll('iframe.lazy-iframe');
const iframeObserver = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
const iframe = entry.target;
iframe.src = iframe.dataset.src;
iframe.classList.remove('lazy-iframe');
iframeObserver.unobserve(iframe);
}
});
});
lazyIframes.forEach(function (iframe) {
iframeObserver.observe(iframe);
});
For <video> elements, consider lazy loading the <source> tags by placing the actual video URL in data-src and only assigning src when the video is close to the viewport. Additionally, you can defer autoplay until the video is visible to avoid unnecessary bandwidth use.
Lazy Loading Background Images
CSS background images are commonly used for hero sections, cards, and decorative elements. They cannot be lazy-loaded with the loading attribute. Instead, use JavaScript to apply the background image only when the element is near the viewport.
<div class="lazy-bg" data-bg="url('https://example.com/bg.jpg')">Content</div>
const lazyBgs = document.querySelectorAll('.lazy-bg');
const bgObserver = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
const el = entry.target;
el.style.backgroundImage = el.dataset.bg;
el.classList.remove('lazy-bg');
bgObserver.unobserve(el);
}
});
}, { rootMargin: '200px 0px' });
lazyBgs.forEach(function (el) {
bgObserver.observe(el);
});
For performance, combine this with a tiny inline SVG or CSS gradient placeholder so the element has visual presence before the real image loads.
Performance Considerations and Best Practices
Implementing lazy loading incorrectly can actually hurt performance and user experience. Follow these guidelines to ensure optimal results:
- Reserve space for lazy-loaded elements — Always specify
widthandheighton images and videos, or use CSS aspect-ratio boxes. This prevents layout shifts (CLS) when the resource eventually loads. - Use a reasonable rootMargin — Pre-load resources just before they enter the viewport (e.g., 100–200px) to eliminate any visible loading delay. Setting the margin too large defeats the purpose of lazy loading.
- Avoid lazy loading above-the-fold content — Images and videos that are visible on initial load should be loaded normally. Lazy loading above-the-fold resources can harm LCP and delay first paint.
- Combine with other optimizations — Lazy loading works best alongside image compression (WebP, AVIF), responsive images (
srcset), and CDN delivery. Use tools like TinyPNG or Squoosh to compress images. - Throttle the observer if needed — For pages with hundreds of elements, consider batching or debouncing the observer callback. However, IntersectionObserver is generally efficient enough even with many observed elements.
- Test on real devices — Simulate slow network conditions (e.g., via Chrome DevTools) and test on low-end mobile devices to verify that lazy loading performs as expected.
Accessibility and Lazy Loading
Lazy loading can introduce accessibility issues if not implemented carefully. Screen readers and keyboard navigation rely on semantic HTML and proper loading order. Here’s how to keep your lazy loading accessible:
- Always include descriptive alt text — Even for lazy-loaded images, the
altattribute must be present and meaningful. Do not rely on the image source to convey information. - Ensure focusable elements remain reachable — If you lazy-load iframes or interactive content, make sure the elements are in the DOM from the start (even if the source is deferred) so that keyboard users can tab to them.
- Provide loading indicators for critical media — If a lazy-loaded image is essential for understanding the content, consider showing a placeholder or a loading spinner to indicate that more content is on the way.
- Test with assistive technologies — Use screen readers like NVDA or VoiceOver to confirm that lazy-loaded resources are announced correctly when they become visible.
For a deeper dive into accessibility patterns with lazy loading, refer to the W3C Web Accessibility Initiative’s Image Tutorial.
Using Native Lazy Loading as a Backup
Modern browsers support the loading="lazy" attribute natively. You can combine both approaches: use JavaScript-based IntersectionObserver for maximum control and cross-browser consistency, but also add loading="lazy" as a progressive enhancement. For browsers that support the native attribute, the browser will handle lazy loading even if JavaScript fails or is disabled. For browsers that don’t, your JS code kicks in.
<img class="lazy"
src="placeholder.jpg"
data-src="image.jpg"
loading="lazy"
alt="..." />
In your JavaScript observer, you can optionally check if img.loading === 'lazy' and skip observing if native lazy loading is already supported. This reduces the overhead of observing many elements when it’s not needed.
Conclusion
JavaScript-based lazy loading remains one of the most effective techniques for improving webpage performance and user experience. By leveraging the Intersection Observer API, you gain fine-grained control over when and how resources are loaded, with minimal impact on the main thread. Remember to always reserve layout space, provide meaningful fallbacks, test across devices, and keep accessibility at the forefront of your implementation.
When applied properly, lazy loading can significantly reduce initial page weight, improve Core Web Vitals, and make your site feel snappier — especially for users on slower networks or mobile devices. Start by lazy loading your largest images and embedded content, then expand the technique to other resource types as your performance budget demands.
For further reading and advanced patterns, explore web.dev’s guide to browser-level lazy loading and the MDN documentation on Intersection Observer.