ScreenshotNeo

BlogHow-to

How to Lazy Load Images for Faster Page Loads

Use native lazy loading for below-the-fold images, keep critical images eager, and reserve space to prevent layout shifts.

By the ScreenshotNeo team4 October 20268 min read

To lazy load an ordinary image below the initial viewport, add loading="lazy" to its <img> element and include accurate width and height values. Keep images that are initially visible, especially the likely Largest Contentful Paint (LCP) image, eager so the browser can request them promptly.

<img
  src="/images/article-photo.jpg"
  width="1200"
  height="800"
  loading="lazy"
  alt="A description of the image"
>

The browser decides when a lazy image is close enough to the viewport to fetch. The HTML attribute does not let you set a pixel threshold. Native lazy loading is widely available and is the simplest choice for ordinary below-the-fold images. MDN: HTMLImageElement loading.

1. Choose which images to defer

Use lazy loading for images that are outside the initial viewport and are not needed to render the page’s first view. The browser may delay their requests, which can avoid fetching images a visitor never reaches and reduce competition with more important early resources.

  • Initially visible or likely LCP: leave eager loading as the default. Do not add loading="lazy".
  • Below the fold and non-critical: use loading="lazy".
  • Uncertain position: inspect the page at its common viewport sizes. Avoid lazy loading an image that can appear in the first view on a smaller screen.

Lazy loading controls when a resource is fetched; it does not compress the image or select a smaller file. Treat loading strategy, image dimensions, responsive source selection, and compression as separate but complementary choices. MDN: the img element.

2. Reserve space to prevent layout shifts

Set intrinsic dimensions on each image, or reserve the same aspect ratio with CSS. The browser can calculate the image’s shape before its file arrives and allocate room in the layout. Without known dimensions, an unloaded lazy image may initially have no useful size, and surrounding content can move when it loads. MDN: img attributes.

<img
  src="/images/article-photo.jpg"
  width="1200"
  height="800"
  loading="lazy"
  alt="A description of the image"
>

The width and height describe the source image’s aspect ratio; CSS can still make it responsive:

.article-photo {
  display: block;
  width: 100%;
  height: auto;
}

If the design crops images into a fixed frame, reserve that frame’s intended ratio in CSS and use object-fit as appropriate. Keep the reserved dimensions consistent with the rendered design to avoid a second layout change.

3. Use responsive image markup when needed

For images that have multiple sizes, provide candidates with srcset and a matching sizes description. The browser can choose a suitable resource without waiting for JavaScript to inspect the viewport. Lazy loading and responsive selection solve different problems: one defers the request, while the other offers appropriate source files. MDN: responsive images.

<img
  src="/images/landscape-1200.jpg"
  srcset="/images/landscape-480.jpg 480w,
          /images/landscape-800.jpg 800w,
          /images/landscape-1200.jpg 1200w"
  sizes="(max-width: 600px) 100vw, 800px"
  width="1200"
  height="800"
  loading="lazy"
  alt="Mountain landscape at sunset"
>

Use <picture> when the page needs art direction, such as a different crop at narrow widths, or format alternatives. Keep a usable <img> fallback inside it. Avoid JavaScript that swaps a large initial src for a smaller one after detecting the viewport: the browser may already have started fetching the first file, creating an unnecessary extra request.

4. Keep the LCP image discoverable and appropriately prioritized

Do not lazy load the likely LCP image. If an important image needs an explicit priority hint, fetchpriority="high" can signal its relative importance to the browser. Use it sparingly: it is a hint, does not make the file smaller, and does not replace selecting a suitable source. Older browsers may not support it. MDN: fetchPriority.

<img
  src="/images/page-hero.jpg"
  width="1600"
  height="900"
  fetchpriority="high"
  alt="The main subject of the page"
>

Usually, leave a critical image’s loading behavior at its eager default. Add the priority hint only when there is a clear reason to prioritize that resource; applying high priority to many images weakens the value of the hint.

5. When to use JavaScript and Intersection Observer

For ordinary image deferral, start with native loading="lazy". Use Intersection Observer when application behavior must respond to an element entering or leaving a viewport, or when you need custom observation logic that the native attribute does not expose. The browser chooses the native lazy-load distance; authors cannot tune it through loading.

A minimal observer-based pattern uses a placeholder URL in data-src, then assigns the real URL when the image intersects. Include dimensions just as you would with native loading.

<img
  class="observe-lazy"
  data-src="/images/article-photo.jpg"
  width="1200"
  height="800"
  alt="A description of the image"
>

<script>
  const images = document.querySelectorAll('img.observe-lazy[data-src]');

  if ('IntersectionObserver' in window) {
    const observer = new IntersectionObserver((entries, activeObserver) => {
      for (const entry of entries) {
        if (!entry.isIntersecting) continue;
        const image = entry.target;
        image.src = image.dataset.src;
        image.removeAttribute('data-src');
        activeObserver.unobserve(image);
      }
    });

    images.forEach((image) => observer.observe(image));
  } else {
    // Fallback: load images when IntersectionObserver is unavailable.
    images.forEach((image) => {
      image.src = image.dataset.src;
      image.removeAttribute('data-src');
    });
  }
</script>

This pattern is for cases that need custom JavaScript behavior. It does not establish a general performance advantage over native loading. If using it, test the fallback, make sure a critical image is not withheld from browser discovery, and handle load or error state if the application depends on it.

6. Check whether images actually finished loading

A lazy image can still be pending when the window’s load event fires. Do not treat that event as proof that every image is ready. If code needs to know about a particular image, check its complete property and, when needed, its natural dimensions:

const image = document.querySelector('#article-photo');

if (image.complete && image.naturalWidth > 0) {
  console.log('Image loaded successfully');
} else {
  image.addEventListener('load', () => console.log('Image loaded'));
  image.addEventListener('error', () => console.error('Image failed to load'));
}

Native lazy loading is deferred only when JavaScript is enabled, as a browser anti-tracking measure. If an image must be available without JavaScript, do not depend on native lazy loading for that requirement. MDN: lazy loading.

7. Measure the result on your page

  1. Record a baseline using the same page, viewport, browser conditions, and network profile you will use after the change.
  2. Apply lazy loading only to non-critical images that start below the initial viewport.
  3. Check the initial view for missing or late-loading images, especially the likely LCP image.
  4. Scroll through the page and confirm deferred images appear correctly and do not cause visible layout shifts.
  5. Compare image requests and page behavior under the same conditions. Results depend on the page, image set, network, and browser; there is no universal percentage improvement to expect.

Lazy loading can avoid early requests for images that are never viewed, but it does not reduce the bytes of an image that is eventually downloaded. Pair it with suitable dimensions, responsive candidates, and separately considered compression or formats. There is no single compression setting or image format that fits every site. MDN: image file type and format guide.

8. Common problems and fixes

Symptom Likely cause Fix
The hero image appears late A visible or likely LCP image has loading="lazy". Remove the lazy attribute. Consider fetchpriority="high" only if the image needs an explicit priority hint.
Text or cards jump when an image appears The browser did not have dimensions or an aspect ratio to reserve space. Set accurate width and height, or reserve the intended ratio in CSS.
Lazy images never appear in a custom implementation The observer callback does not run, the real URL is wrong, or JavaScript failed. Check the console and network request, verify data-src, and provide a fallback when Intersection Observer is unavailable.
An image seems to download twice JavaScript replaces a source after the browser already started downloading the initial one. Use responsive srcset/sizes or <picture> so the browser selects a source from the markup.
An image is still loading after the window load event Lazy resources can remain deferred beyond that event. Observe the image’s own load/error events or check complete and naturalWidth.
Native lazy loading does not defer requests with scripts disabled Browsers defer native lazy loading only when JavaScript is enabled. Do not rely on native lazy loading for a no-JavaScript requirement; ensure important content remains available under the site’s fallback behavior.

9. Quick checklist

  • Use loading="lazy" for non-critical images below the initial viewport.
  • Keep initially visible and likely LCP images eager.
  • Give images accurate dimensions or reserve their aspect ratio.
  • Use srcset and sizes for responsive candidates; use <picture> for art direction or format alternatives.
  • Use Intersection Observer only when custom viewport behavior is needed.
  • Measure with consistent conditions, and check both the initial view and scrolled content.

10. Or skip the browser setup

If you need a screenshot of a page while developing or documenting it, ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns an image or PDF; the API supports PNG, JPEG, and WebP output. See the ScreenshotNeo documentation for request options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
  • Cookie and consent banners, newsletter popups, and chat widgets are removed before capture; each cleanup step can be turned off.
  • Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed. Response headers report the page verdict and billing status.
  • An MCP server provides take_screenshot, get_page_info, and capture_pdf for AI agents and MCP clients.
  • The free plan includes 1,000 screenshots a month with no card. Paid plans start at $5 for 3,000 screenshots.

Sign up for ScreenshotNeo’s free plan: 1,000 screenshots a month, no card required.

11. FAQ

Does loading="lazy" improve page speed?

It can reduce early image requests when used on off-screen images, but it does not shrink files or guarantee a particular speed gain. Measure your own page.

Can I choose how many pixels before an image loads?

No. With native lazy loading, the browser determines the fetch distance; the HTML attribute has no threshold setting.

Should every image use lazy loading?

No. Keep images needed in the initial view eager and defer non-critical images below it.

Is JavaScript required?

Native lazy loading is deferred only when JavaScript is enabled. A JavaScript observer is needed only for custom behavior beyond the native attribute.