HTML Images & Visual Content Image Lazyloading in HTML

If you have ever opened a webpage and sat there watching a little spinning loader for what felt like forever, there is a good chance the developer did not bother with lazy loading. And honestly? I get it. When you are new to HTML, just getting images to show up feels like a win. Performance feels like someone else’s problem.

But here is the thing: once you understand lazy loading images, you will never go back. It is one of the simplest optimizations you can add to any webpage, it is built right into HTML, and it genuinely makes a real difference for your users.

In this tutorial, we are going to cover everything: what lazy loading is, how to use it with a single HTML attribute, when to use loading="eager" instead, how it connects to Core Web Vitals, and what browser support looks like. Let us get into it.


01What Is Lazy Loading, Really?

The name says it all. Lazy loading means: do not load something until you actually need it.

When your browser opens a webpage, it normally fetches every single image on that page, even the ones that are buried all the way at the bottom, images your user might never even scroll to. That is a lot of wasted bandwidth, wasted time, and a slower experience for no real reason.

With HTML lazy loading, you tell the browser: “Hey, only load this image when the user is about to see it.” The browser then holds off on fetching that image until the user scrolls close enough that it is about to enter the viewport.

The result? Your page loads faster upfront, uses less data, and still shows every image right when the user needs it. No spinning loaders, no blank spaces (when done correctly). Just a snappier experience.

Lazy Loading vs No Lazy Loading
Without Lazy Loading
Viewport (visible)
Hero Image: loaded
Section Image 1: loaded
Below viewport
Section Image 2: loaded anyway
Footer Image: loaded anyway
Blog Thumbnail 1: loaded anyway
Blog Thumbnail 2: loaded anyway

Result: All 6 images downloaded immediately, even if the user never scrolls down.

With Lazy Loading
Viewport (visible)
Hero Image: loaded
Section Image 1: loaded
Below viewport
Section Image 2: waiting…
Footer Image: waiting…
Blog Thumbnail 1: waiting…
Blog Thumbnail 2: waiting…

Result: Only 2 images downloaded upfront. The rest load as the user scrolls to them.


02What Actually Happens When a Browser Loads Images

Before we look at the code, it helps to understand what a browser does by default.

When a browser parses your HTML, it reads the page from top to bottom. Every time it encounters an <img> tag, it queues a network request to download that image, regardless of where it sits on the page. A page with 20 images fires 20 download requests almost simultaneously.

On a fast connection, this might be fine. But for users on slower connections, mobile networks, or older devices, that is a big hit. The visible content takes longer to show up because the browser is busy downloading images the user has not even scrolled to yet.

This is exactly the problem HTML lazy loading solves. And the fix is one attribute.


03Native HTML Lazy Loading with the loading Attribute

HTML5 gave us a native way to lazy load images without any JavaScript, third-party libraries, or complicated setup. You just add one attribute to your <img> tag.

The loading=”lazy” Syntax

It really is this simple:

<img
  src="https://daniyaldev.com/wp-content/uploads/2026/01/social-share-scaled.png"
  alt="A descriptive text about this image"
  width="800"
  height="450"
  loading="lazy"
>

That single loading="lazy" attribute tells the browser: do not download this image until the user scrolls close enough to see it.

No JavaScript. No plugin. No configuration. Just HTML.

The loading attribute accepts three values:

Value What It Does When to Use It
lazy Defers loading until the image is near the viewport. The browser decides the exact threshold. Any image below the fold, in a long article, or in a list of cards.
eager Loads the image immediately, regardless of position. This is the browser’s default behavior. Hero images, logos, and anything visible without scrolling.
auto Lets the browser decide. In practice, most browsers treat this the same as eager. Not recommended to use explicitly. Generally avoid using this directly. Let the browser default.

What the Browser Does Behind the Scenes

When the browser encounters loading="lazy", it does not skip the image entirely. It registers that the image exists, notes its position, and then uses an internal threshold, typically a few hundred pixels before the image enters the viewport, to decide when to start the download.

This means by the time the user actually scrolls to the image, it is usually already loaded or loading. Done well, the user never sees a blank space or a flash of nothing.

The exact threshold varies by browser. Chrome, for example, uses different thresholds depending on the connection speed. On a slow connection, it starts loading images earlier to compensate. You do not control this directly, and that is fine. The browser handles it intelligently.


04When loading=”eager” Is the Right Move

Here is the part most tutorials rush past, and it is actually really important.

Not every image should be lazy loaded. Applying loading="lazy" to every image on your page, including your hero image and above-the-fold content, will actually hurt your performance, not help it.

When you lazy load an image that is immediately visible on page load, the browser delays fetching it. The user sees a blank space or a layout shift while the image loads. That is a bad experience and it also tanks your Core Web Vitals score, specifically your LCP (Largest Contentful Paint). We will get into that shortly.

The Above the Fold Dividing Line

“Above the fold” is an old newspaper term that means the content visible without scrolling. In web terms, it is everything in the user’s viewport when the page first loads.

The rule here is simple: images above the fold should not be lazy loaded. Images below the fold should.

Page Structure

Above the fold (visible on load)
🖼️ Hero / Banner Image: loading=”eager” (or omit)
🏷️ Logo: loading=”eager” (or omit)
The Fold Line (bottom of viewport)
Below the fold (user scrolls to see)
🖼️ Section / Article Image: loading=”lazy” ✓
🃏 Blog Card Thumbnails: loading=”lazy” ✓
🖼️ Gallery Images: loading=”lazy” ✓
📸 Footer Image: loading=”lazy” ✓

The fold line is not a fixed pixel value. It depends on the user’s screen size and browser window. Think of it as: anything a user sees without touching the scroll bar.

Quick rule of thumb: if you have to ask yourself “is this image below the fold?”, it probably is, and lazy loading it is safe.


05Always Set Width and Height With Lazy Loading

This is the mistake I see most often, and it causes a specific kind of headache called layout shift.

When an image is lazy loaded, the browser does not know its dimensions until it starts downloading it. If you have not told the browser how large the image is, the surrounding content will shift and jump around as the image loads in. It looks broken, it feels broken, and it hurts your CLS (Cumulative Layout Shift) score in Core Web Vitals.

The fix is to always set width and height attributes on every image, lazy loaded or not. This lets the browser reserve the right amount of space before the image arrives.

<!-- Bad: no dimensions, layout will shift -->
<img src="photo.jpg" alt="A landscape photo" loading="lazy">

<!-- Good: dimensions set, space reserved, no shift -->
<img
  src="https://daniyaldev.com/wp-content/uploads/2026/01/social-share-scaled.png"
  alt="A landscape photo"
  width="800"
  height="450"
  loading="lazy"
>

We covered this in detail in our article on mastering images in HTML5, where we talked about why width and height matter for every image you place on a page. With lazy loading, it becomes even more critical.


06Lazy Loading with the picture Element

If you are already using the <picture> element for responsive images or format switching (and you probably should be if you are serving WebP), lazy loading works exactly the same way. You add the loading attribute to the inner <img> tag, not to the <picture> tag itself.

<picture>
  <source
    srcset="image.webp"
    type="image/webp"
  >
  <img
    src="https://daniyaldev.com/wp-content/uploads/2026/01/social-share-scaled.png"
    alt="A descriptive alt text"
    width="800"
    height="450"
    loading="lazy"
  >
</picture>

The <picture> element is just a container. The browser still uses the <img> as the actual image element, so that is where loading="lazy" goes.

If you are not yet comfortable with the <picture> element, our picture tag in HTML tutorial walks through everything from art direction to WebP fallbacks with clean examples.

The same rule applies for srcset and sizes as well:

<img
  src="https://daniyaldev.com/wp-content/uploads/2026/01/social-share-scaled.png"
  srcset="
    image-400.jpg 400w,
    image-800.jpg 800w,
    image-1200.jpg 1200w
  "
  sizes="(max-width: 600px) 100vw, 800px"
  alt="A responsive image with lazy loading"
  width="800"
  height="450"
  loading="lazy"
>

Every approach: standard <img>, <picture>, and srcset, all support the loading attribute the exact same way.


07Lazy Loading, Core Web Vitals, and Your Page Score

This is where lazy loading gets really interesting, because it connects directly to how Google measures and ranks your page’s performance.

Core Web Vitals are a set of real-world performance metrics that Google uses to evaluate user experience. Three metrics matter here specifically:

  • LCP (Largest Contentful Paint): How fast the largest visible content element loads. Usually a hero image or heading.
  • CLS (Cumulative Layout Shift): How much the page layout jumps around during loading.
  • FID / INP: Interaction responsiveness. Less directly affected by image loading.

LCP: Never Lazy Load Your Hero Image

Your LCP element is almost always your hero image. It is the biggest thing on the page, visible immediately, and Google is timing how fast it loads.

If you add loading="lazy" to your hero image, you are literally telling the browser to wait before downloading the most important visual on your page. Your LCP score tanks. Your search ranking takes a hit.

This is a real mistake people make. They add loading="lazy" to every image tag on the page with a quick find-and-replace, and wonder why their Lighthouse score gets worse.

Here is how to think about it: lazy loading helps performance by deferring what does not need to load immediately. Your hero image absolutely needs to load immediately. So skip the lazy attribute there, and consider adding fetchpriority="high" to really give it a boost:

<!-- Hero image: no lazy loading, high fetch priority -->
<img
  src="https://daniyaldev.com/wp-content/uploads/2026/01/social-share-scaled.png"
  alt="Our main hero image"
  width="1200"
  height="600"
  fetchpriority="high"
>

<!-- Section image lower on the page: lazy loaded -->
<img
  src="https://daniyaldev.com/wp-content/uploads/2026/01/social-share-scaled.png"
  alt="A section illustration"
  width="800"
  height="450"
  loading="lazy"
>

fetchpriority="high" tells the browser to treat this image as a high-priority resource and download it before lower-priority stuff. It is a relatively new attribute, but browser support is solid in all modern browsers.

CLS: How Dimensions Prevent Layout Shift

We touched on this earlier, but it is worth reinforcing because CLS is a Core Web Vitals metric specifically hurt by lazy loading without dimensions.

When content jumps around while a page loads, that is CLS. It happens because the browser does not know how much space to reserve for an incoming image. The moment the image starts loading and the browser knows its size, everything on the page shifts to make room.

Setting width and height attributes lets the browser calculate the aspect ratio ahead of time using modern CSS (browsers automatically apply aspect-ratio based on these values). The space is reserved from the start, and nothing shifts when the image arrives.

LCP
Largest Contentful Paint
Measures when the biggest visible element loads. Target: under 2.5 seconds.
Never apply loading=”lazy” to your LCP image. Use fetchpriority=”high” instead.
📐
CLS
Cumulative Layout Shift
Measures unexpected layout shifts during loading. Target: under 0.1.
Always set width and height on every lazy-loaded image to prevent layout shift.
🖼️
FCP
First Contentful Paint
Measures when any content first appears. Target: under 1.8 seconds.
Lazy loading off-screen images frees up bandwidth, helping above-fold content paint faster.

Good lazy loading actually improves LCP and FCP indirectly. By not downloading off-screen images upfront, the browser can prioritize your visible content and critical resources. The page feels faster because the important stuff arrives sooner.


08Browser Support for HTML Lazy Loading

Here is the good news: browser support for native HTML lazy loading is excellent in 2024 and beyond.

Chrome, Edge, Firefox, Safari, and Opera all support the loading attribute natively. We are looking at global browser coverage well above 95%.

🌐
Chrome

77+

🦊
Firefox

75+

🧭
Safari

15.4+

🔷
Edge

79+

🔴
Opera

64+

The one thing worth knowing: in browsers that do not support the loading attribute (very old versions), they simply ignore it. The images load normally, eagerly, the same way they always did. So adding loading="lazy" is completely safe in unsupported browsers. It degrades gracefully with zero breakage.

A Clean JavaScript Fallback for Older Browsers

If for some reason you need to explicitly support very old browsers and want a proper lazy loading behavior in them, you can use a small JavaScript fallback with the IntersectionObserver API. Here is a clean, minimal version:

<!-- Use data-src instead of src for the fallback approach -->
<img
  data-src="https://daniyaldev.com/wp-content/uploads/2026/01/social-share-scaled.png"
  src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
  alt="A section illustration"
  width="800"
  height="450"
  loading="lazy"
  class="lazy-img"
>

<script>
  // Only run if the browser does not support native lazy loading
  if ('loading' in HTMLImageElement.prototype === false) {
    const images = document.querySelectorAll('img.lazy-img');

    if ('IntersectionObserver' in window) {
      const observer = new IntersectionObserver(function(entries) {
        entries.forEach(function(entry) {
          if (entry.isIntersecting) {
            const img = entry.target;
            img.src = img.dataset.src;
            img.classList.remove('lazy-img');
            observer.unobserve(img);
          }
        });
      });

      images.forEach(function(img) {
        observer.observe(img);
      });
    } else {
      // IntersectionObserver not supported either: just load everything
      images.forEach(function(img) {
        img.src = img.dataset.src;
      });
    }
  }
</script>

A few things to notice here:

The placeholder src is a tiny 1×1 transparent GIF encoded as base64. This prevents the browser from showing a broken image icon while the real image is pending. It is essentially invisible but valid.

The script checks 'loading' in HTMLImageElement.prototype first. If the browser supports native lazy loading, the JavaScript does absolutely nothing. No overhead, no interference. The native behavior takes over.

Honestly, for most projects in 2024 onwards, you do not need this fallback at all. The native loading="lazy" attribute has such wide support that the JavaScript fallback is mostly for very specialized legacy requirements.


09Common Mistakes That Will Hurt Your Performance

I want to walk through the most common lazy loading mistakes because I have seen each of these in real projects, and they are all easy to fix once you know about them.

1
Lazy loading your hero or LCP image
The single most impactful mistake. Your hero image is usually the LCP element. Adding loading="lazy" to it delays the most important image on your page and directly hurts your Core Web Vitals score. Leave the hero image without a loading attribute, or explicitly add loading=”eager”.
2
Skipping width and height attributes
Without dimensions, the browser cannot reserve space for the image. When it loads in, content shifts, your CLS score gets hit, and the experience looks broken. Always pair loading=”lazy” with width and height.
3
Forgetting alt text on lazy-loaded images
Lazy loading does not remove the requirement for good alt text. Screen readers, search engines, and situations where images fail to load all rely on alt text. Every image needs a meaningful alt attribute. Check our alt text guide if you want a refresher.
4
Lazy loading images in the first few scroll positions
Even if an image is technically below the fold by a small amount, lazy loading it can cause a visible delay if the user scrolls quickly. For images near the top of the page (within the first screen or two), eager loading is safer.
5
Using lazy loading as a substitute for image optimization
Lazy loading defers loading. It does not compress your images. A 4MB image loaded lazily is still a 4MB download when the user scrolls to it. Always optimize your images (WebP, correct dimensions, compression) alongside lazy loading.

10A Complete Real-World Example

Let us put everything together. Here is what a properly written blog post article page looks like, with correct lazy loading applied in the right places:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>My Blog Post</title>
</head>
<body>

  <!-- ============================================
       HEADER: Logo is above the fold, load eagerly
       ============================================ -->
  <header>
    <img
      src="logo.png"
      alt="My Website Logo"
      width="140"
      height="40"
      loading="eager"
    >
  </header>

  <!-- ============================================
       HERO: LCP element, never lazy load this one.
       fetchpriority="high" makes it a top priority.
       ============================================ -->
  <section class="hero">
    <img
      src="https://daniyaldev.com/wp-content/uploads/2026/01/social-share-scaled.png"
      alt="A stunning hero image for the blog post"
      width="1200"
      height="600"
      fetchpriority="high"
    >
    <h1>My Amazing Blog Post Title</h1>
  </section>

  <!-- ============================================
       ARTICLE CONTENT: Below the fold, lazy load
       ============================================ -->
  <article>
    <p>Article intro paragraph goes here...</p>

    <!-- First content image: safe to lazy load -->
    <img
      src="https://daniyaldev.com/wp-content/uploads/2026/01/social-share-scaled.png"
      alt="An illustration explaining the first concept"
      width="800"
      height="450"
      loading="lazy"
    >

    <p>More article content here...</p>

    <!-- Responsive image with srcset: also lazy loaded -->
    <picture>
      <source
        srcset="diagram.webp 1x, [email protected] 2x"
        type="image/webp"
      >
      <img
        src="https://daniyaldev.com/wp-content/uploads/2026/01/social-share-scaled.png"
        alt="A diagram of the process"
        width="800"
        height="500"
        loading="lazy"
      >
    </picture>
  </article>

  <!-- ============================================
       RELATED POSTS GRID: Definitely below the fold
       ============================================ -->
  <section class="related-posts">
    <h2>Related Articles</h2>
    <div class="posts-grid">

      <article class="post-card">
        <img
          src="https://daniyaldev.com/wp-content/uploads/2026/01/social-share-scaled.png"
          alt="Thumbnail for related post 1"
          width="400"
          height="225"
          loading="lazy"
        >
        <h3>Related Post 1</h3>
      </article>

      <article class="post-card">
        <img
          src="https://daniyaldev.com/wp-content/uploads/2026/01/social-share-scaled.png"
          alt="Thumbnail for related post 2"
          width="400"
          height="225"
          loading="lazy"
        >
        <h3>Related Post 2</h3>
      </article>

      <article class="post-card">
        <img
          src="https://daniyaldev.com/wp-content/uploads/2026/01/social-share-scaled.png"
          alt="Thumbnail for related post 3"
          width="400"
          height="225"
          loading="lazy"
        >
        <h3>Related Post 3</h3>
      </article>

    </div>
  </section>

</body>
</html>

Notice the pattern clearly:

  • The logo: loading="eager" because it is at the top of every page.
  • The hero image: no loading attribute (defaults to eager), plus fetchpriority="high" to make it a priority resource for LCP.
  • Every other image below the fold: loading="lazy", always paired with width and height.

That structure alone, applied consistently, can have a meaningful impact on your page load performance and your Core Web Vitals scores.


11Quick Reference: Lazy Loading Cheat Sheet

Lazy Loading Quick Reference
Everything you need at a glance
loading="lazy"
Defers image loading until near the viewport. Use on all below-fold images.
loading="eager"
Loads the image immediately. Use on hero images, logos, and above-fold content.
fetchpriority="high"
Boosts the download priority of an image. Use on your LCP element (hero image).
Always set width + height
Prevents layout shift (CLS) by letting the browser reserve space before the image loads.
Works with <picture>
Add loading="lazy" to the inner <img> tag, not the <picture> element.
Works with srcset
Fully compatible. Just add the attribute to the <img> tag as normal.
Unsupported browsers
Ignore the attribute completely and load all images eagerly. Perfectly safe, no breakage.
Browser support
Chrome 77+, Firefox 75+, Safari 15.4+, Edge 79+. Coverage over 95% globally.

12A Note on Lazy Loading and Accessibility

One quick thing before we wrap up: lazy loading itself does not affect accessibility in a meaningful way. Screen readers and assistive technologies read your alt text from the HTML structure, not from the loaded image file. So as long as you are writing good alt text (which we covered in detail in our alt text for images tutorial), lazy loading will not impact the experience for users with screen readers.

The one indirect consideration: if you are building a single-page application or dynamically injecting images via JavaScript, make sure dynamically added images also carry the loading="lazy" attribute. The attribute is evaluated at the time the element is parsed, so dynamically inserted images will respect it just as well as statically written ones.


13Bringing It All Together

Lazy loading images is one of those features where the effort-to-reward ratio is almost unfairly good. One attribute, a few seconds per image, and you get meaningfully better page load performance, improved Core Web Vitals scores, and a better experience for your users, especially those on slower connections.

The core ideas to carry with you:

  • Use loading="lazy" on every image below the fold.
  • Never lazy load your hero image. Use fetchpriority="high" there instead.
  • Always pair lazy loading with width and height attributes to prevent layout shift.
  • It works natively in all modern browsers and degrades safely in older ones.
  • Lazy loading and image optimization work together. Do both.

If you want to go deeper on image performance, our article on responsive images with srcset and sizes pairs really well with everything you just learned here. Combining responsive images with lazy loading is one of the most effective things you can do for front-end performance right now.

And if you have been following along with this series, the next step is understanding image dimensions and aspect ratios in HTML, which connects directly to the layout shift concepts we touched on here. Until then, go open one of your HTML files and start adding loading="lazy" where it belongs. Small change, real impact.

Image Lazyloading in HTML

Choose a difficulty to load your challenge

Loading challenge…

Ready to Practice?

Select a difficulty level above to load your challenge.

Easy

1
Live Preview
Great Work!