Tag: Web Performance

  • Optimize Next.js Images for Performance

    Optimize Next.js Images for Performance

    Short answer: To optimize Next.js images for performance, use the built-in next/image component, which automatically serves responsive images, lazy loads them, and converts to modern formats like WebP. Configure remote URLs in next.config.js, set appropriate sizes, and use priority for above-the-fold images.

    Key takeaways

    • Always use next/image for automatic optimization.
    • Enable lazy loading for all images by default.
    • Configure remote hostnames in next.config.js.
    • Set explicit width and height to prevent layout shifts.
    • Use priority for above-the-fold images.
    • Serve modern formats like WebP and AVIF.

    Images are often the heaviest assets on a web page. In Next.js, you can optimize images for performance using the built-in next/image component. This component automatically applies lazy loading, responsive sizing, and modern image formats. In this article, you will learn practical steps to optimize Next.js images and improve your site’s speed and user experience.

    Why Image Optimization Matters in Next.js

    Large images slow down page load times and hurt Core Web Vitals, especially Largest Contentful Paint (LCP). Next.js provides the next/image component to handle optimization automatically. It resizes images, serves them in next-gen formats like WebP and AVIF, and lazy loads them by default. This reduces bandwidth usage and improves performance.

    Without optimization, a 2 MB hero image can delay your LCP by several seconds. With next/image, the same image can be served as a 200 KB WebP file, dramatically improving load time.

    A digital illustration of image compression and optimization icons
    Understanding image optimization is key to performance. — Photo: Firmbee / Pixabay

    Getting Started with next/image

    To use the component, import Image from next/image. You can use it for local images stored in your project or remote images from external URLs.

    Using with Local Images

    For local images, just import the file and pass it to the src prop. Next.js automatically optimizes these images.

    import Image from 'next/image';
    import heroImage from '../public/hero.jpg';
    
    export default function Page() {
      return (
        <Image
          src={heroImage}
          alt="Hero image"
          width={1200}
          height={600}
        />
      );
    }
    

    Using with Remote Images

    For remote images, you need to configure the allowed hostnames in next.config.js. This is a security measure.

    // next.config.js
    module.exports = {
      images: {
        remotePatterns: [
          {
            protocol: 'https',
            hostname: 'example.com',
            port: '',
            pathname: '/images/**',
          },
        ],
      },
    };
    

    Then you can use the remote URL in the src prop. Note that you must provide width and height to prevent layout shifts.

    How Lazy Loading Improves Performance

    The next/image component lazy loads images by default. This means images are only loaded when they enter the viewport, saving bandwidth on initial page load. For above-the-fold images, you can disable lazy loading using the priority prop.

    Lazy loading is implemented using the native loading="lazy" attribute and Intersection Observer. It ensures that below-fold images do not block the initial render.

    Setting Responsive Sizes and Breakpoints

    To serve the right image size for each device, use the sizes and fill props. The sizes prop tells the browser how large the image will be at different breakpoints, helping select the best source.

    <Image
      src="/hero.jpg"
      alt="Hero"
      fill
      sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
    />
    

    The fill prop makes the image fill its parent container. You must set the parent to position: relative and define its size.

    A responsive image displayed on different device screens for optimization
    Use responsive sizes to serve the right image for each device. — Photo: gefrorene_wand / Pixabay

    Using Priority for Above-the-Fold Images

    The priority prop tells Next.js to preload the image and skip lazy loading. Use it for the largest above-the-fold image, usually the hero image. This improves LCP because the image starts loading immediately.

    <Image
      src="/hero.jpg"
      alt="Hero"
      width={1200}
      height={600}
      priority
    />
    

    Only use priority for one or two images to avoid overwhelming the network.

    Comparing Image Formats: JPEG vs WebP vs AVIF

    Modern formats like WebP and AVIF provide better compression than JPEG. Next.js automatically negotiates the format based on the browser’s Accept header. You can control this with the formats configuration.

    Format Compression Browser Support Recommended Use
    JPEG Good All Fallback
    WebP Better Widely supported Primary format
    AVIF Best Supported in many modern browsers For supported browsers

    By default, Next.js offers WebP and AVIF. You can customize the allowed formats in next.config.js.

    Additional Optimization Techniques

    Image Caching and CDN

    Next.js caches optimized images in the .next/cache/images directory. For production, consider using a CDN to serve images with cache headers. You can configure custom image loaders to use a CDN like Cloudinary or Imgix.

    Blur Up and Placeholder

    The placeholder prop allows you to show a blurred version of the image while it loads. This improves perceived performance. Use blur for local images or data URL for remote images.

    <Image
      src={heroImage}
      alt="Hero"
      placeholder="blur"
    />
    

    Common Mistakes to Avoid

    Many developers forget to configure remotePatterns for external images, causing errors. Others omit width and height, leading to layout shifts. Avoid using large images without setting sizes, as the browser may download oversized images. Also, do not apply priority to every image; it defeats lazy loading.

    Always test your images with Lighthouse to verify optimization. Check the “Properly size images” audit to see if you serve correctly sized images.

    How to Debug Image Optimization Issues

    When images don’t look right or performance isn’t improving, you can debug using Next.js’s built-in image optimization endpoint. Check the URL of the optimized image: it usually has /_next/image?url=.... If the image is not being optimized, ensure you are using the Image component and not a regular <img> tag. For remote images, verify that the hostname is in remotePatterns and that the image URL is accessible. Use the browser’s network tab to inspect the image request and response headers. Look for content-type to confirm it’s WebP or AVIF. Also check the response status — a 404 means the URL is wrong or the image doesn’t exist.

    Another common issue is that the image size doesn’t match the layout. Use the onLoadingComplete callback to get the actual image dimensions and compare them with what you expect. This helps identify if sizes or width and height are misconfigured.

    Optimizing for Different Screen Sizes and Devices

    To deliver the best experience across mobile, tablet, and desktop, combine the sizes prop with CSS media queries. The sizes attribute tells the browser what width the image will display at each breakpoint. For example, for a full-width hero on mobile and a half-width image on desktop, use sizes="(max-width: 768px) 100vw, 50vw". Always test on real devices to confirm the correct image is downloaded. You can check the image’s “current src” in the browser’s inspector to see which URL the browser selected. If it picks a too-large image, your sizes attribute might be too broad.

    Remember that the Image component generates multiple resized versions of each source image. The default device sizes are: 640, 750, 828, 1080, 1200, 1920, 2048, and 3840 pixels. You can customize these in next.config.js with the imageSizes and deviceSizes options. Adjust them based on your breakpoints and the maximum width of your layout.

    Frequently asked questions

    Does next/image automatically convert images to WebP?

    Yes, the next/image component automatically serves images in WebP and AVIF formats when the browser supports them. It negotiates the format based on the Accept header, so you don’t need to manually convert images.

    Do I need to set width and height for all next/images?

    Yes, you should always set width and height to prevent layout shifts and improve Cumulative Layout Shift (CLS). The only exception is when using the fill prop, where the parent container defines the dimensions.

    How do I use next/image with images from a CMS?

    You can use the remote image approach: configure the CMS domain in remotePatterns in next.config.js, then pass the image URL to the src prop. Ensure you provide width, height, and alt text.

    Can I use next/image with SVG files?

    Yes, but the next/image component does not optimize SVGs because they are vector-based. It will pass the SVG through without resizing or converting formats. You can use an tag or inline instead.

    What is the priority prop and when should I use it?

    The priority prop tells Next.js to preload the image and skip lazy loading. Use it for the largest above-the-fold image (usually the hero) to improve LCP. Avoid using it on multiple images to prevent unnecessary bandwidth usage.

  • Lazy Loading for Better Web Performance

    Lazy Loading for Better Web Performance

    Short answer: Lazy loading delays loading of offscreen images, videos, and iframes until the user scrolls near them. It reduces initial page weight, speeds up load times, and saves bandwidth. Modern browsers support native lazy loading via the loading=’lazy’ attribute on img and iframe elements.

    Key takeaways

    • Native lazy loading with loading=’lazy’ is the simplest implementation.
    • Intersection Observer offers more control for custom lazy loading.
    • Always include width and height attributes to prevent layout shifts.
    • Lazy load images below the fold; keep above-the-fold images eager.
    • Apply lazy loading to videos, iframes, and background images too.
    • Measure performance gains using Lighthouse or WebPageTest.

    Imagine landing on a web page that feels instant. Content appears as you scroll, images load just in time, and you never stare at a blank white box. That’s the promise of lazy loading—a technique that defers loading non-visible resources until the user needs them. For frontend developers, implementing lazy loading is one of the most impactful ways to improve web performance without a massive overhaul.

    Developer writing lazy loading code on a laptop
    Implementing lazy loading with native attributes or JavaScript. — Photo: Innovalabs / Pixabay

    What Is Lazy Loading and Why Does It Matter?

    Lazy loading means postponing the loading of resources that are not immediately needed. Instead of downloading every image, video, or iframe when the page first loads, the browser only loads what’s visible above the fold. The rest are fetched as the user scrolls down.

    Why does this matter? Because download size directly affects load time. A typical webpage today weighs over 2 MB, with images accounting for the largest share. By deferring offscreen images, you can reduce initial page weight by 30% or more. This leads to faster Time to Interactive, better Largest Contentful Paint (LCP), and lower data usage for mobile users.

    User expectations are high: they expect pages to load in under two seconds. Lazy loading helps you meet that benchmark without compromising on visual richness.

    Native Lazy Loading: The Easiest Way to Start

    The simplest method is native lazy loading using the loading attribute. Add loading="lazy" to your <img> and <iframe> tags, and the browser handles the rest. It’s supported in all modern browsers since 2020.

    <img src="large-photo.jpg" alt="A scenic mountain landscape" loading="lazy" width="800" height="600">

    Always include width and height attributes. This prevents Cumulative Layout Shift (CLS) because the browser reserves the space before the image loads. Without explicit dimensions, lazy loaded images can cause layout jumps when they finally appear.

    For iframes, the approach is identical:

    <iframe src="https://example.com/map" loading="lazy" width="600" height="450"></iframe>

    Native lazy loading works well for most use cases. However, it gives you limited control. If you need to add custom effects like fading in images or tracking load events, you’ll want JavaScript-based solutions.

    Intersection Observer: Custom Lazy Loading with JavaScript

    The Intersection Observer API lets you efficiently detect when an element enters the viewport. It’s more powerful than older scroll-event-based approaches because it doesn’t cause layout thrashing.

    Basic Implementation

    Here’s a minimal example that lazy loads images using Intersection Observer:

    const images = document.querySelectorAll('img[data-src]');
    
    const observer = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
            if (entry.isIntersecting) {
                const img = entry.target;
                img.src = img.dataset.src;
                img.removeAttribute('data-src');
                observer.unobserve(img);
            }
        });
    });
    
    images.forEach(img => observer.observe(img));

    Use data-src to store the actual image URL. When the image enters the viewport, swap it into the src attribute and stop observing it to free memory.

    Adding a Blur-Up or Placeholder Effect

    A common enhancement is to show a tiny, blurred placeholder while the full image loads. Use two images: a tiny compressed version (e.g., 20px wide) as a background or separate <img>, and lazy load the full-resolution one on top. The placeholder loads instantly, and the crisp image fades in later.

    Here’s a practical implementation using CSS and Intersection Observer:

    <div class="lazy-image-wrapper">
        <img class="placeholder" src="tiny-blur.jpg" alt="">
        <img class="lazy" data-src="full-image.jpg" alt="Description" width="800" height="600">
    </div>
    
    <style>
    .lazy-image-wrapper { position: relative; overflow: hidden; }
    .lazy-image-wrapper img { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; }
    .placeholder { filter: blur(10px); transform: scale(1.1); }
    .lazy { opacity: 0; transition: opacity 0.3s; }
    .lazy.loaded { opacity: 1; }
    </style>
    
    <script>
    const lazyImages = document.querySelectorAll('.lazy');
    const observer = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
            if (entry.isIntersecting) {
                const img = entry.target;
                img.src = img.dataset.src;
                img.onload = () => img.classList.add('loaded');
                observer.unobserve(img);
            }
        });
    }, { rootMargin: '200px' });
    lazyImages.forEach(img => observer.observe(img));
    </script>

    The placeholder image should be extremely small in file size (under 1KB). The effect creates a smooth visual transition and improves perceived performance.

    When to Use Intersection Observer Over Native

    Use the Intersection Observer when you need: custom loading animations, load event callbacks, lazy loading of background images, or support for older browser versions (though native is now widely supported).

    Web performance dashboard showing improved metrics after lazy loading
    Measure the impact of lazy loading on your performance scores. — Photo: JillWellington / Pixabay

    Lazy Loading Videos and Iframes

    Lazy loading isn’t just for images. Videos and third-party embeds (like YouTube or maps) can also be deferred.

    Video Lazy Loading

    For HTML video elements, set preload="none" and use a poster image. Then, use Intersection Observer to start loading the video when the user scrolls near it. Avoid autoplaying videos with sound; they hurt performance and user experience.

    <video preload="none" poster="thumbnail.jpg" width="640" height="360">
        <source src="video.mp4" type="video/mp4">
    </video>

    One common mistake with video lazy loading is not handling the source element correctly. If you dynamically set the src on the video element, ensure you also trigger loading by calling video.load() after assigning the source. Otherwise, the browser may not fetch the video.

    Iframe Lazy Loading

    Third-party widgets like social media feeds or maps often contain heavy JavaScript. Defer them with loading="lazy" on the iframe, or for older browsers, add the src attribute only when the element comes into view.

    For YouTube embeds, a clean technique is to replace the iframe with a clickable thumbnail image. When the user clicks, you swap in the real iframe and start playback. This reduces initial page weight significantly and only loads the YouTube player on demand.

    <div class="youtube-placeholder" data-embed="dQw4w9WgXcQ">
        <img src="thumbnail.jpg" alt="Video title" width="560" height="315">
        <button class="play-button">Play</button>
    </div>
    
    <script>
    document.querySelectorAll('.youtube-placeholder').forEach(placeholder => {
        placeholder.addEventListener('click', () => {
            const iframe = document.createElement('iframe');
            iframe.src = `https://www.youtube.com/embed/${placeholder.dataset.embed}?autoplay=1`;
            iframe.width = 560;
            iframe.height = 315;
            iframe.allow = 'autoplay';
            placeholder.innerHTML = '';
            placeholder.appendChild(iframe);
        });
    });
    </script>

    This approach also avoids the performance hit of loading the YouTube player for every visitor, even those who never play the video.

    Lazy Loading Background Images

    CSS background images are not covered by the native loading attribute. To lazy load them, you need JavaScript. A common technique is to set a data-background attribute on elements and swap it in using Intersection Observer:

    <div class="hero" data-background="url('hero.jpg')"></div>
    
    <script>
    const bgElements = document.querySelectorAll('[data-background]');
    const observer = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
            if (entry.isIntersecting) {
                const el = entry.target;
                el.style.backgroundImage = el.dataset.background;
                el.removeAttribute('data-background');
                observer.unobserve(el);
            }
        });
    });
    bgElements.forEach(el => observer.observe(el));
    </script>

    One thing to watch out for: background images can cause layout shifts if their container has no intrinsic dimensions. Always set explicit heights or aspect ratios on these containers to reserve space.

    Comparison: Native vs. Intersection Observer

    Feature Native Lazy Loading Intersection Observer
    Browser Support Modern browsers only Polyfillable, wider support
    Ease of Implementation Very easy – one attribute Moderate – requires JS
    Customization None Full control
    Performance Excellent, browser-optimized Good, but slight overhead
    Built-in Layout Shift Prevention Needs explicit dimensions Needs explicit dimensions
    Background Images Not supported Works with JavaScript
    Event Tracking No load events Can track when loaded

    For most projects, start with native lazy loading and only add Intersection Observer if you need advanced features.

    Best Practices and Common Pitfalls

    Always Specify Dimensions

    Lazy loading without width and height leads to layout shifts. Modern CSS can help with responsive images: use aspect-ratio or max-width: 100%; height: auto; on images that have explicit width and height attributes. For containers, set aspect-ratio to match the content dimensions to reserve space.

    Don’t Lazy Load Above-the-Fold Content

    Critical images (hero banners, logos, primary content) should load eagerly. Add loading="eager" or simply omit the attribute. Lazy loading above-the-fold images can delay LCP and hurt perceived performance. A good rule of thumb: if an image is visible on initial load, load it normally.

    Set Appropriate Thresholds

    With Intersection Observer, set a reasonable rootMargin to start loading images before they enter the viewport (e.g., rootMargin: '200px'). This ensures images are ready when the user scrolls. Be careful not to set too large a margin—loading everything too early defeats the purpose of lazy loading.

    Test with Real Users and Tools

    Use Lighthouse, WebPageTest, or Chrome DevTools to verify that lazy loading is working correctly. Check that images are not deferred unnecessarily and that no visible content flickers or shows empty space. Pay attention to the LCP element: it should not be lazy loaded.

    Handle JavaScript Disabled Users

    If you rely on JavaScript for lazy loading, provide fallback content (like a <noscript> tag with the full image) so users without JavaScript still see the content. For background images, consider using a default background that works without JS.

    Performance Gains: What to Expect

    Lazy loading can reduce initial page weight significantly. In a typical blog with many images, you might see Time to Interactive drop by 20-30%. Bandwidth usage decreases, especially on mobile networks. However, lazy loading does not improve perceived performance if not implemented correctly—test both with and without it.

    Monitor key metrics: LCP should remain under 2.5 seconds, CLS below 0.1, and First Contentful Paint under 1.8 seconds. Lazy loading primarily affects LCP and initial load time, not CLS (if you use dimensions) or FCP.

    Testing Your Lazy Loading Implementation

    Before shipping, test on a throttled network (e.g., Fast 3G in Chrome DevTools) and verify that images load as you scroll. Check the Network panel to confirm that offscreen images are not requested until they come into view. Also, ensure that the placeholder images appear immediately and that there are no blank gaps.

    One common issue: if you use loading="lazy" on images that are above the fold, you might see a delay in LCP. Use Performance panel to identify these cases and remove lazy loading from critical images.

    Another test: load the page with JavaScript disabled. If you used Intersection Observer, users should still see content via <noscript> fallbacks. Without fallbacks, you risk blank pages for a non-trivial portion of users.

    Implementing lazy loading is a practical step you can take today. Start with native attributes for images and iframes, then progressively enhance with Intersection Observer for custom behaviors. Your users will notice faster load times, and your performance scores will thank you.

    Frequently asked questions

    Does lazy loading affect SEO?

    Lazy loading itself does not hurt SEO if implemented correctly. Search engines like Google can discover lazy loaded images as long as they are in the HTML markup (e.g., using data-src) and the page is rendered properly. Avoid hiding content from crawlers by using proper fallbacks or server-side rendering.

    Can I lazy load background images in CSS?

    Yes, you can lazy load CSS background images using JavaScript. One approach is to initially set the background-image to none, then apply the actual image URL via Intersection Observer when the element enters the viewport. Native lazy loading does not apply to CSS background images.

    Is native lazy loading supported in all browsers?

    Native lazy loading is supported in all major modern browsers: Chrome, Firefox, Edge, and Safari since 2020. Internet Explorer and older versions of Safari do not support it. For those, you can use a polyfill or fall back to Intersection Observer.

    What is the difference between lazy loading and deferred loading?

    Lazy loading defers resources until they are needed (usually when the user scrolls near them), while deferred loading delays resource loading until after the page has loaded, often used for JavaScript scripts. Deferred scripts still load eventually, but lazy loading only triggers on demand.

    Should I lazy load images inside a carousel or slider?

    Yes, lazy loading images inside carousels is recommended if the carousel has many slides that the user may not see. Load only the first few slides eagerly, then lazy load the rest as the user navigates. This reduces initial payload and improves performance.

  • How to Improve Web Performance with Lazy Loading

    How to Improve Web Performance with Lazy Loading

    Short answer: Lazy loading delays loading of non-critical resources until they are needed, typically when they enter the viewport. It reduces initial page weight, lowers bandwidth usage, and improves key metrics like Largest Contentful Paint (LCP) and First Input Delay (FID). Native lazy loading via the loading=”lazy” attribute is the simplest method for images and iframes, while Intersection Observer works for custom elements.

    Key takeaways

    • Lazy loading reduces initial page weight and improves load times.
    • Use native loading=”lazy” for images and iframes whenever possible.
    • Intersection Observer offers more control for custom lazy loading.
    • Always include width and height attributes to prevent layout shifts.
    • Lazy load below-the-fold content first; eager-load critical images.
    • Test with real users using Lighthouse and Web Vitals.

    Slow page loads frustrate users and hurt your search rankings. One of the most effective ways to speed up your site is lazy loading — deferring resources that aren’t immediately visible until they’re about to enter the viewport. This cuts down on initial data transfer, reduces CPU work, and makes your pages feel snappy. In this article, you’ll learn how lazy loading works, how to implement it with modern browser features and JavaScript, and what pitfalls to avoid.

    What Is Lazy Loading and Why Does It Matter?

    Lazy loading is a strategy that postpones the loading of non-critical resources — like images deep down the page, third-party embeds, or heavy scripts — until the moment they’re needed. Instead of downloading everything upfront, the browser loads only what’s in the initial viewport. The rest is loaded on demand.

    Why does this matter for performance? For starters, it reduces the total bytes transferred on the initial load. That directly improves your Largest Contentful Paint (LCP) because the main content isn’t competing with dozens of off-screen images. It also reduces CPU idle time and network congestion, which can lower your First Input Delay (FID) and Cumulative Layout Shift (CLS). Tools like Google Lighthouse heavily reward lazy loading for images and iframes.

    Native Lazy Loading: The Simplest Approach

    Developer writing JavaScript code for Intersection Observer lazy loading implementation
    Intersection Observer provides fine-grained control over lazy loading. — Photo: Boskampi / Pixabay

    The easiest way to lazy load images and iframes is to use the loading attribute. Adding loading="lazy" to an <img> or <iframe> tells the browser to defer loading until the element is close to the viewport. No JavaScript required.

    <img src="hero.jpg" alt="Hero image" loading="lazy" width="1200" height="800">

    For browsers that support it, native lazy loading is fast, efficient, and respects user preferences like data-saver mode. Always include explicit width and height attributes to prevent layout shifts while the image loads. If a browser doesn’t support loading, the image loads eagerly by default, so it’s safe to use today.

    When to Avoid Native Lazy Loading

    Critical above-the-fold images should never be lazy loaded. Doing so can hurt LCP because the browser may delay loading the hero image. Use loading="eager" or omit the attribute entirely for your most important visuals. A good rule of thumb is to lazy load any image that appears below the initial viewport.

    Using Intersection Observer for Custom Lazy Loading

    Bar chart comparing page load times before and after lazy loading implementation
    Measuring performance gains after implementing lazy loading. — Photo: Pexels / Pixabay

    Native lazy loading works great for images and iframes, but for other resources — like background images, custom elements, or lazy-loading a <picture> with multiple sources — you’ll want the Intersection Observer API. It provides a performant way to detect when an element enters the viewport.

    Here’s a basic example that replaces a placeholder data-src with the real src:

    const lazyImages = document.querySelectorAll('img[data-src]');
    
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const img = entry.target;
          img.src = img.dataset.src;
          img.onload = () => img.classList.add('loaded');
          observer.unobserve(img);
        }
      });
    }, { rootMargin: '200px 0px' });
    
    lazyImages.forEach(img => observer.observe(img));

    The rootMargin option triggers loading 200px before the element enters the viewport, which gives a smoother user experience. You can adjust this based on your layout.

    Lazy Loading Background Images

    Background images set via CSS can also be lazy loaded. Store the background URL in a data-bg attribute and apply it when the element becomes visible:

    const lazyBg = document.querySelectorAll('[data-bg]');
    
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const el = entry.target;
          el.style.backgroundImage = `url(${el.dataset.bg})`;
          observer.unobserve(el);
        }
      });
    });
    
    lazyBg.forEach(el => observer.observe(el));

    This technique works well for hero sections that appear further down the page or for card backgrounds in a long list.

    Lazy Loading JavaScript and Third-Party Embeds

    Not just images — you can lazy load scripts too. Heavy third-party libraries, analytics, maps, or social media embeds can block the main thread. Deferring them until after the page is interactive improves TBT (Total Blocking Time) and FID.

    One common pattern is to use <script defer> for non-critical scripts, but for truly lazy-loaded scripts, you can create them dynamically when needed:

    function loadScript(src) {
      const script = document.createElement('script');
      script.src = src;
      script.defer = true;
      document.body.appendChild(script);
    }
    
    // Load when user scrolls to a certain section
    window.addEventListener('scroll', () => {
      if (/* condition met */) {
        loadScript('https://example.com/widget.js');
      }
    }, { once: true });

    For iframes, native loading="lazy" works well, but you can also use Intersection Observer to swap placeholder content. YouTube embeds can be replaced with a static thumbnail and only load the full player on click.

    Best Practices and Common Mistakes

    Lazy loading isn’t a magic bullet. Done wrong, it can hurt performance and user experience. Here are some guidelines:

    • Always set width and height on images and iframes to reserve space and prevent CLS.
    • Lazy load below-the-fold content only. Critical images should load eagerly.
    • Avoid lazy loading everything. If an image is not visible on any screen size (e.g., hidden behind a tab), consider not loading it at all.
    • Test with real users. Use Lighthouse, Web Vitals, and real user monitoring (RUM) to ensure lazy loading isn’t delaying important content.
    • Provide good placeholders. Low-quality image placeholders (LQIP) or blur-up techniques improve perceived performance.

    One common mistake is lazy loading a hero image because it reduces LCP — but it actually increases it. Another is forgetting to update loading attributes when you change layout. Always review which images are above the fold on different devices.

    Comparison: Native vs. Intersection Observer

    Feature Native Lazy Loading Intersection Observer
    Browser Support Modern browsers (Chrome, Firefox, Edge, Safari 15.4+) Wide support in all modern browsers
    Implementation Effort Minimal – just add attribute Moderate – requires JavaScript
    Customization Limited (root margin via loading=”lazy” only) Full control via options and callbacks
    Use Cases Images and iframes Any resource (backgrounds, scripts, custom elements)
    Performance Browser-optimized, no JavaScript overhead Slightly more overhead but still performant

    Choose native lazy loading whenever you can. It’s simpler and faster. Reserve Intersection Observer for cases where native doesn’t cover your needs.

    Lazy Loading in Responsive Images

    When using the <picture> element with multiple sources, native lazy loading applies to the entire element. But you might want finer control. For example, you can combine Intersection Observer with the sizes attribute to load only the appropriate image for the viewport. Store different resolutions in data-srcset and switch them when the element is near the viewport. This avoids loading a desktop-sized image on mobile.

    <picture>
      <source media="(min-width: 800px)" data-srcset="large.jpg">
      <img data-src="small.jpg" alt="Example" width="800" height="600">
    </picture>

    In your observer, swap both srcset on the source elements and src on the img. This ensures you get the right image size for the device while still lazy loading.

    Handling Edge Cases: Print, Accessibility, and Caching

    Lazy loading can interfere with printing if images haven’t loaded yet. Add a CSS print rule to force all lazy images to become eager when printing:

    @media print {
      img[loading="lazy"], [data-src] {
        content: attr(data-src);
        /* force eager loading via JS */
      }
    }

    You can also use JavaScript to load all lazy resources when the user invokes print via window.beforeprint event. For accessibility, ensure that screen readers can still navigate lazy-loaded content. The loading attribute is accessible, but if you use JavaScript, make sure the resources are loaded when the focus reaches them, perhaps by observing focus events or using a preload buffer.

    Caching is another consideration. Browsers cache lazy-loaded images after they load, so subsequent visits or scrolls won’t re-fetch them. But if you lazy load scripts, ensure they have proper cache headers to avoid blocking future loads. Test with browser DevTools on throttled connections to verify that lazy loaded resources appear quickly when needed.

    Measuring the Impact

    After implementing lazy loading, measure the difference. Run Lighthouse before and after, and look at these metrics:

    • LCP: Should improve if critical images load faster without competition.
    • Total Image Bytes: Reduced on initial load.
    • Number of Requests: Fewer requests upfront.
    • Time to Interactive: Should decrease as less JavaScript is parsed.

    Also monitor real-user metrics like FID or INP. If you lazy load too aggressively, users may experience delays when scrolling. Adjust your rootMargin to trigger loads sooner if needed.

    For more on building performant layouts, check out our guide on Common CSS Grid Mistakes and How to Fix Them. Proper layout techniques complement lazy loading by reducing layout shifts.

    Conclusion: Start Small, Measure Often

    Lazy loading is a practical, high-impact performance optimization. Begin with native lazy loading on all images and iframes below the fold. Then explore Intersection Observer for more complex cases like background images or third-party scripts. Always measure before and after with Lighthouse and real-user monitoring. Avoid lazy loading critical above-the-fold assets, and always set dimensions to prevent layout shifts. With these techniques, you’ll deliver faster, smoother experiences for your users.

    Frequently asked questions

    What is lazy loading in web performance?

    Lazy loading is a technique that defers the loading of non-critical resources until they are needed, typically when they scroll into the viewport. It reduces initial page weight, speeds up load time, and improves performance metrics like LCP and FID.

    How do you lazy load images in HTML?

    You can lazy load images by adding the loading=’lazy’ attribute to the img tag. Browsers that support the attribute will defer loading until the image is close to the viewport. Always include width and height attributes to prevent layout shifts.

    Should I lazy load above-the-fold images?

    No, you should not lazy load critical above-the-fold images. Doing so can delay the Largest Contentful Paint (LCP). Eager-load hero images and other visible content, and lazy load only images below the initial viewport.

    What is the difference between native lazy loading and Intersection Observer?

    Native lazy loading uses the HTML loading attribute and is supported by modern browsers with zero JavaScript overhead. Intersection Observer is a JavaScript API that provides more control and works for any element, including background images and scripts, but requires more code.

    Does lazy loading affect SEO?

    Lazy loading can positively affect SEO by improving page speed and user experience, which are ranking factors. However, ensure that content is still accessible to search engine crawlers. Native lazy loading is generally safe, and you should avoid hiding important content behind lazy loading that isn’t triggered by crawlers.