ScreenshotNeo

BlogGuides

Lazy Loading Images: Best Practices for Faster Websites

Learn when to lazy-load images, how to protect LCP and prevent layout shifts, and how to measure the results on real pages.

By the ScreenshotNeo team4 October 20269 min read

Use the browser’s native loading="lazy" attribute for images that start below the fold. Keep images visible in the first viewport, especially the likely Largest Contentful Paint (LCP) image, eagerly loadable so the browser can request them promptly. Give every image intrinsic dimensions or an equivalent aspect ratio so the browser can reserve space, optimize image bytes separately, and measure the page before and after.

Lazy loading changes when an image is requested. It does not resize or compress the file, and it does not guarantee a faster page by itself.

1. What lazy loading does

Without lazy loading, the browser may fetch images as it discovers them in the document, including images far below the part a visitor initially sees. Native lazy loading lets the browser defer eligible off-screen image requests until they are close enough to be useful. The browser chooses that distance; HTML does not provide a setting for it.

This can reduce early network work on image-heavy pages, particularly when visitors do not scroll through the whole page. MDN describes lazy-loading below-the-fold images as one of the biggest improvements for many websites, while emphasizing that image delivery and page behavior still matter. See MDN’s image guide and MDN’s lazy-loading guide.

2. Decide which images should be lazy-loaded

Image position or role Recommended loading Reason
Below-the-fold article, catalog, or gallery image loading="lazy" It is not needed to render the initial viewport.
Hero image, logo, or image visible on initial load Default eager loading (omit the attribute or set loading="eager") Deferring a visible image can postpone its request and delay display.
Likely LCP image Eager; consider fetchpriority="high" when measurement supports it The browser should discover and prioritize this important resource early.
Image whose position changes across screen sizes Choose based on the layout at the relevant viewport; verify on mobile and desktop An image below the fold on one layout may be visible immediately on another.

The Google Chrome team’s browser-level image lazy-loading guide advises: “For images that are visible when the user first loads the page, and especially for LCP images, use the browser’s default eager loading so they can be available right away.” Do not put loading="lazy" on every image as a blanket template rule.

3. Add native lazy loading in HTML

For a below-the-fold content image, include loading="lazy" and its real intrinsic dimensions:

<img
  src="/images/gallery-1.webp"
  alt="A red bicycle beside a brick wall"
  width="1200"
  height="800"
  loading="lazy"
>

Use dimensions that reflect the source image’s intrinsic width and height. CSS can still size it responsively:

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

For an image that is visible immediately, leave out loading or explicitly mark it eager. If it is confirmed as the important LCP resource, you may signal its priority:

<img
  src="/images/hero.webp"
  alt="A mountain trail at sunrise"
  width="1600"
  height="900"
  fetchpriority="high"
>

fetchpriority="high" is a prioritization hint, not a replacement for checking the LCP element and request timing. Avoid elevating many images: competing high-priority downloads can undermine prioritization. If the important image is discoverable only after CSS or script processing, the web.dev LCP guide discusses improving discovery, including preloading where appropriate.

Images inside picture

Put the loading attribute on the fallback <img> inside <picture>:

<picture>
  <source
    type="image/avif"
    srcset="/images/article-640.avif 640w, /images/article-1280.avif 1280w"
    sizes="(max-width: 700px) 100vw, 700px"
  >
  <source
    type="image/webp"
    srcset="/images/article-640.webp 640w, /images/article-1280.webp 1280w"
    sizes="(max-width: 700px) 100vw, 700px"
  >
  <img
    src="/images/article-1280.jpg"
    alt="A close view of a ceramic bowl"
    width="1280"
    height="853"
    loading="lazy"
  >
</picture>

The browser selects a supported source while the fallback <img> carries the loading behavior and dimensions.

4. Reserve layout space to prevent shifts

When the browser knows an image’s dimensions before it downloads, it can reserve the right amount of space. Without that reservation, content below an image can jump when the image arrives, causing layout shift. Dimensions can also help the browser avoid treating many not-yet-sized images as zero-sized items near the viewport.

Use width and height attributes, or reserve the ratio in CSS when that fits your component system:

.product-card__image {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
}

Prefer correct intrinsic dimensions on the HTML image as the basic, broadly understandable signal. Make sure the ratio matches the image; an incorrect ratio reserves the wrong shape and may still produce a visible adjustment.

5. Native lazy loading or JavaScript?

Approach Use it when Trade-off
Native loading="lazy" Ordinary off-screen images on modern browsers Simple and browser-managed, but the page cannot set the trigger distance. Browsers that do not support it ignore the attribute, so images still load rather than breaking.
JavaScript with IntersectionObserver A concrete requirement needs a custom trigger, fallback behavior, or application-specific loading state More code and failure modes; a late trigger can delay images, while a too-early trigger reduces the deferral benefit.

Use native behavior by default. Check compatibility for your actual audience if legacy browser support is required; a fallback library may be justified there. Do not add a JavaScript lazy-loading package just because images need deferral.

Optional custom trigger with IntersectionObserver

If the application genuinely needs a custom threshold, keep real image URLs in data attributes until the observer approaches them. This example starts loading up to 256 pixels before the viewport as an illustrative buffer, not a universal optimum:

<img
  class="deferred-image"
  src="/images/placeholder.svg"
  data-src="/images/gallery-1.webp"
  alt="A red bicycle beside a brick wall"
  width="1200"
  height="800"
>
<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);
      }
    }, { rootMargin: "0px 0px 256px 0px" });

    images.forEach(image => observer.observe(image));
  } else {
    // Fallback: load the real source so unsupported browsers still show the image.
    images.forEach(image => {
      image.src = image.dataset.src;
      image.removeAttribute("data-src");
    });
  }
</script>

For production use, ensure the placeholder has the same reserved dimensions, handle image errors if your interface needs a fallback, and test keyboard, no-script, and browser-support requirements. Do not use this pattern for initially visible or LCP images. Browser-native lazy loading is simpler when custom control is unnecessary.

6. Make image files lighter separately

Lazy loading postpones a request; it does not reduce the bytes transferred when an image is eventually fetched. Pair it with suitable formats, compression, and responsive image dimensions:

  • Serve an image close to the rendered size instead of sending a very large source to a small slot.
  • Use srcset and sizes for resolution choices, or <picture> when offering alternate formats or art direction.
  • Choose a format and quality level appropriate to the image and audience, then inspect visual results.
  • Use caching and a content delivery path suited to your site’s architecture.

MDN’s image guide reports historical image-weight growth from 2011 to 2019; those figures illustrate why image payloads deserve attention, but they are not current page averages or a prediction for your site. Measure your own transferred bytes.

7. Measure whether the change helped

  1. Pick representative pages, including image-heavy pages and pages with a prominent hero image.
  2. Identify the LCP element with browser developer tools or Lighthouse. Confirm whether it is an image and when its request begins.
  3. Record a baseline: LCP, layout shifts, initial image requests, transferred image bytes, and behavior on mobile and desktop.
  4. Apply lazy loading only to below-the-fold images, while reserving image space and optimizing payloads separately.
  5. Repeat measurements on representative devices and network conditions. Compare the same page state and note whether the visitor scrolls.
  6. Check field data as well as lab measurements when available; a synthetic run cannot represent every visitor or browsing path.

web.dev states a good-experience LCP target of 2.5 seconds or less for at least 75% of page visits. This is a Core Web Vitals target, not a result promised by adding a loading attribute. See the LCP optimization guide.

8. Common problems and fixes

Symptom Likely cause Fix
The hero appears late or LCP gets worse The initially visible or LCP image was marked lazy Remove loading="lazy" from that image. Check when its request starts; consider fetchpriority="high" or preload only if evidence shows discovery or prioritization is a problem.
Content jumps as images load No dimensions or aspect ratio was reserved, or the ratio is wrong Set correct width/height attributes or a matching CSS aspect-ratio.
Images below the fold still download early They are inside the browser’s chosen near-viewport region, the viewport/layout differs, or another script requests them Inspect the network waterfall and image initiators. Native threshold is browser-managed; use a custom observer only for a demonstrated need.
Images never appear in an older browser A custom data-src implementation has no fallback, or the browser does not support a required API Use native loading where sufficient, or ensure unsupported browsers receive real src URLs through a tested fallback.
Page feels no faster despite fewer initial requests The LCP image, scripts, fonts, server response, or image byte size may be the actual bottleneck Inspect the LCP element and request timing; optimize the measured bottleneck. Lazy loading only defers eligible image requests.
Some images remain blank after custom lazy loading Bad URL, failed request, observer initialization issue, or JavaScript error Check the console and network response, validate each data URL, and add an error fallback or eager fallback path appropriate to the application.

9. Reliability, performance, and cost considerations

Native lazy loading has little application code to fail and lets the browser adapt request timing. It may not defer an image until the exact point you expect because its threshold is intentionally browser-controlled. Test with actual layouts, since responsive changes can move image positions. Custom observers provide control but require correct fallback handling and can produce blank or late content when scripts fail or thresholds are poorly chosen.

The main benefit is avoiding or postponing work for images a visitor may not reach. Scrolling visitors still need the images, and their eventual downloads still cost bandwidth and can affect rendering near the scroll position. Image compression, responsive sizing, cache policy, and delivery infrastructure address transfer size and availability separately. There is no universal speed gain to quote: use measurements on the pages and devices that matter.

10. Or skip the browser setup

If your goal is to capture a page image while checking how the rendered page looks, ScreenshotNeo is a website screenshot API and MCP server for developers. It is not a replacement for implementing lazy loading on your site. One GET request captures a URL as an image or PDF; the returned capture can help you inspect a page state.

Use the API key from your account and consult the ScreenshotNeo API documentation for options and response details:

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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const bytes = new Uint8Array(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes));
  • Cookie banners, popups, and chat widgets are removed before the shot; each cleanup step can be turned off.
  • Bot checks, blank pages, timeouts, failed loads, 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 tools for Claude, Cursor, and other MCP clients.
  • The free plan includes 1,000 shots per month with no card. Paid plans start at $5 for 3,000 shots; all features are on every plan.

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

11. FAQ

Does loading=”lazy” work without JavaScript?

Yes. It is a browser-level HTML feature and does not require page JavaScript for supported browsers. Unsupported browsers ignore the attribute and load the image normally.

Should every image have loading=”lazy”?

No. Restrict it to images that begin off-screen. Eagerly load initially visible images, especially the LCP image.

Can I control how close an image must be before native lazy loading starts?

No. The browser chooses the threshold. Use an observer with a measured custom buffer only when a specific requirement calls for that control.

Does lazy loading improve SEO?

It is a loading strategy, not an SEO guarantee. Keep important content images discoverable and ensure deferred images load correctly when users and crawlers need them; measure user experience rather than assuming a ranking effect.

Sources