ScreenshotNeo

BlogHow-to

How to Lazy Load Images in HTML

Use HTML’s native `loading="lazy"` attribute to defer off-screen images. Learn when to use it, how it works with responsive images, and how to avoid common pitfalls.

By the ScreenshotNeo team29 September 20268 min read

How to Lazy Load Images in HTML

To lazy load an ordinary image in HTML, add loading="lazy" to its <img> element. The browser then defers fetching it until it is near the viewport. Set the image’s dimensions so the browser can reserve its space, and leave images needed in the initial view eager.

<img
  src="photo.jpg"
  alt="A red bicycle leaning against a brick wall"
  width="800"
  height="600"
  loading="lazy"
>

This is the browser-native approach: no JavaScript or library is needed for standard images. The browser decides how close to the viewport is close enough to start loading, so lazy loading does not necessarily wait until an image is visibly on screen. See MDN’s image element reference for the attribute and markup details.

1. Decide which images should load lazily

Lazy loading is most useful for images below the initial viewport that are not needed immediately. It can avoid fetching assets a reader never reaches and reduce the page’s initial network work. The effect depends on the page, image sizes and count, connection, and how far a reader scrolls; there is no universal speed improvement to promise.

The browser begins fetching a lazy image when it is near the viewport, at a distance chosen by the browser.
The browser begins fetching a lazy image when it is near the viewport, at a distance chosen by the browser.
  • Use loading="lazy" for non-critical images farther down a long article, gallery, or results page.
  • Keep critical images eager. Images visible on first render, especially a prominent lead image, are usually better left at the default eager behavior. Lazy loading a critical image can delay its appearance.
  • Do not apply lazy loading indiscriminately. Choose based on whether the image is needed soon, rather than adding the attribute to every image in a template.

If an important image is discovered late or appears slowly, investigate its markup and priority separately. The fetchpriority attribute is a separate priority hint; it does not replace the decision about whether an image should be deferred.

2. Add the attribute and reserve space

For each off-screen image, provide a source, meaningful alternative text, intrinsic dimensions, and the loading hint:

Intrinsic dimensions reserve an image’s aspect ratio before its file loads and help prevent layout shifts.
Intrinsic dimensions reserve an image’s aspect ratio before its file loads and help prevent layout shifts.
<img
  src="/images/team-at-work.jpg"
  alt="Two people assembling a model at a workbench"
  width="1200"
  height="800"
  loading="lazy"
>

The width and height describe the image’s intrinsic dimensions in pixels. They let the browser determine its aspect ratio and reserve layout space before the file arrives, which helps prevent content from jumping when it loads. If your CSS scales images, preserve that ratio:

img {
  max-width: 100%;
  height: auto;
}

You can instead reserve space with CSS aspect-ratio or a fixed-size container, but make sure the layout has a nonzero expected height before the image loads. This matters especially for lazy content: an unloaded image without dimensions can have zero size, leaving the browser nothing useful to bring into view.

Write alternative text according to the image’s purpose. Informative images need a concise description; decorative images should use alt="". Lazy loading does not change accessibility requirements.

3. Use lazy loading with responsive images

Responsive image selection still works with native lazy loading. A browser can choose an appropriate source from srcset and sizes; the loading attribute controls when fetching begins, while the responsive attributes help choose which resource to fetch.

<img
  src="/images/river-800.jpg"
  srcset="/images/river-400.jpg 400w,
          /images/river-800.jpg 800w,
          /images/river-1600.jpg 1600w"
  sizes="(max-width: 600px) 100vw, 800px"
  width="1600"
  height="1067"
  alt="A river winding through a forest"
  loading="lazy"
>

The width and height should express the same aspect ratio as the selected image variants. In supported implementations, sizes="auto" can be used together with lazy loading so the browser can use the expected rendered width. Include a fallback size for implementations that do not use the auto value:

<img
  src="/images/river-800.jpg"
  srcset="/images/river-400.jpg 400w,
          /images/river-800.jpg 800w,
          /images/river-1600.jpg 1600w"
  sizes="auto, (max-width: 600px) 100vw, 800px"
  width="1600"
  height="1067"
  alt="A river winding through a forest"
  loading="lazy"
>

For art direction—different crops at different breakpoints—use <picture> and <source> elements, with the actual <img> carrying the loading hint, dimensions, and alt text:

<picture>
  <source
    media="(max-width: 600px)"
    srcset="/images/canyon-portrait.jpg"
  >
  <img
    src="/images/canyon-wide.jpg"
    width="1200"
    height="800"
    alt="A canyon at sunset"
    loading="lazy"
  >
</picture>

Keep the fallback image usable and ensure the chosen crop preserves the meaning of the image. For syntax and support details, consult MDN’s responsive image and sizes documentation.

4. Use Intersection Observer only when you need custom behavior

For ordinary images, native loading is the simplest choice. An IntersectionObserver implementation is useful when application behavior needs to run as an element approaches view—for example, to reveal a placeholder, trigger a custom animation, or coordinate loading with application state. Avoid replacing the native behavior with custom code unless there is a concrete requirement.

This minimal pattern stores the deferred source in data-src, observes each image, and assigns its source when it intersects. It also stops observing after the source is assigned:

<img
  class="deferred-image"
  src="/images/placeholder.svg"
  data-src="/images/gallery-01.jpg"
  width="1200"
  height="800"
  alt="A sailboat on a calm lake"
>

<script>
  const images = document.querySelectorAll("img.deferred-image[data-src]");

  if ("IntersectionObserver" in window) {
    const observer = new IntersectionObserver((entries, currentObserver) => {
      for (const entry of entries) {
        if (!entry.isIntersecting) continue;

        const image = entry.target;
        image.src = image.dataset.src;
        image.removeAttribute("data-src");
        currentObserver.unobserve(image);
      }
    });

    images.forEach((image) => observer.observe(image));
  } else {
    // Compatibility fallback: load all deferred images.
    images.forEach((image) => {
      image.src = image.dataset.src;
      image.removeAttribute("data-src");
    });
  }
</script>

This is a starting point, not a drop-in replacement for every image pipeline. Production code may need to handle srcset, sizes, loading and error states, placeholder cleanup, and framework lifecycle changes. The fallback above loads all images for browsers without Intersection Observer. MDN discusses native lazy loading, Intersection Observer, and event-based alternatives in its lazy loading guide.

5. Verify behavior and interpret load events

Inspect the page in a browser’s network tools while scrolling. Confirm that below-the-fold image requests are deferred and that the intended responsive source is selected. Test at multiple viewport sizes and with a slower connection profile; a layout that looks correct on a wide desktop can expose missing dimensions on mobile.

Do not assume the window load event means every lazy image has finished downloading. Deferred resources can load later as they approach the viewport. If your application needs to know whether a particular image is available, observe that image’s load and error events or inspect its complete property with care: a completed request may also have failed, so pair it with an appropriate success check such as natural dimensions.

6. Common problems and fixes

Symptom Likely cause Fix
An image appears late near the viewport The browser controls its preload distance; it is not required to wait until the image is visible. For a critical image, remove loading="lazy". For a custom threshold, use an observer with an appropriate root margin and measure the result.
A blank gap or layout jump appears No dimensions or aspect ratio were reserved before the file loaded. Add accurate width and height attributes, or reserve the ratio in CSS.
An image never appears The URL may be wrong, the request may fail, a script-based loader may not have run, or an observer may not be observing the intended element. Check the network response and console, verify the source URL, and ensure the observer setup runs after the image elements exist.
Images fail when JavaScript is disabled Native lazy image loading is deferred only when JavaScript is enabled, as an anti-tracking measure described by MDN. Do not promise native lazy loading in a no-JavaScript context. If the page must show images without scripting, use ordinary image markup without depending on a script-only placeholder loader.
Every image downloads on initial load The markup may omit the attribute, images may already be near the viewport, or a custom script may assign all deferred sources immediately. Inspect the rendered DOM and requests; confirm the loader does not eagerly assign every source.
Responsive image downloads are too large srcset descriptors or sizes may not describe the rendered image slot accurately. Correct the candidate widths and slot sizes, then inspect which candidate the browser selects at relevant viewport and density settings.

7. Performance, reliability, and cost

Native lazy loading has low implementation overhead because it is a browser hint on standard markup. Deferring off-screen downloads can save bandwidth and work when readers do not scroll to those images. It does not make image files smaller, guarantee a faster initial render in every layout, or set a universal loading threshold. Responsive sources, correctly sized assets, and a stable layout still matter.

For reliability, keep real image URLs in markup when possible, reserve their layout space, and ensure your page remains understandable if a request fails. Custom script loaders add code paths that can fail due to initialization order, selector mistakes, or source handling. Test those paths if you choose them. No special service or paid software is required to use the native HTML attribute.

8. Capture a page to inspect its images

A screenshot can help document how a page renders at a particular viewport after its images load. For an automated visual check, capture the target page and compare the result with your expected layout. ScreenshotNeo is a website screenshot API and MCP server for developers; its site describes one-request captures, while the API documentation lists configuration options.

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

For a useful check, capture at the viewport and device scale relevant to your page, and allow the page enough time to render. A screenshot shows a rendered state; it does not prove every deferred image has been fetched or that the page behaves correctly with JavaScript disabled. Use browser network inspection when you need to verify request timing.

Or skip the browser setup

ScreenshotNeo can return a screenshot with one request. Cookie banners, popups, and chat widgets are removed before the shot; bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card, and paid plans start at $5 for 3,000.

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)

See the ScreenshotNeo API docs for request options, then sign up for 1,000 free screenshots a month with no card.

FAQ

Does loading="lazy" apply to CSS background images?

No. The HTML attribute belongs on an <img> element. Background images need a different loading strategy, such as CSS or application logic coordinated with visibility.

Can I tell the browser exactly how many pixels before the viewport to load?

Not with the native attribute. The browser chooses its threshold. Use a custom observer only if your interface requires control over when your code begins loading or revealing content.

Does lazy loading change what a screen reader announces?

The loading hint does not replace accessible text. Keep meaningful alt text for informative images and empty alt text for decorative ones.

Can I use native lazy loading and an observer together?

You can, but avoid having two independent mechanisms compete to assign sources. Pick one loading owner for each image, or use the observer only for a separate behavior such as a visual reveal.