Image Lazyloading in HTML
Learn using lazy loading for images in HTML with loading="lazy", using eager loading, Core Web Vitals optimization, browser support, and performance best practices.
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.
Result: All 6 images downloaded immediately, even if the user never scrolls down.
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
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.
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%.
77+
75+
15.4+
79+
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.
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”.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
loadingattribute (defaults to eager), plusfetchpriority="high"to make it a priority resource for LCP. - Every other image below the fold:
loading="lazy", always paired withwidthandheight.
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
loading="lazy"loading="eager"fetchpriority="high"width + height<picture>loading="lazy" to the inner <img> tag, not the <picture> element.srcset<img> tag as normal.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
widthandheightattributes 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
Loading challenge…
Ready to Practice?
Select a difficulty level above to load your challenge.