How to Implement Lazy Loading in HTML and JavaScript
Lazy loading is one of the simplest page speed improvements you can make, and one of the easiest to get wrong. Done well, a page downloads only what is on screen and fetches the rest as the visitor scrolls, which saves data and helps the first view appear sooner. Done badly, it delays the hero image, makes content jump as images arrive, or hides images and text from search engines. This guide explains what lazy loading is in web development, how to implement it in HTML with the loading attribute, how to implement it in JavaScript with IntersectionObserver for backgrounds and video, what you should never lazy load, and how to check the result on WordPress, Shopify and Next.js sites.
What lazy loading is in web development
Lazy loading means delaying the download of a resource until it is likely to be needed. Instead of fetching every image, video and embedded map as soon as the page opens, the browser fetches only what is on or near the screen, and the rest as the visitor scrolls towards it.
The opposite is eager loading, which is what browsers do by default. A long article with forty photos downloads all forty at once, even if most visitors read the first few paragraphs and leave. Lazy loading cuts that waste. The page uses less data, the important first-screen files face less competition for bandwidth, and phones on slow connections show the top of the page sooner.
There are two ways to implement it:
- Native lazy loading in HTML, with the
loading="lazy"attribute on<img>and<iframe>elements. The browser decides when to load. No JavaScript is needed. - Lazy loading in JavaScript, usually with the
IntersectionObserverAPI, which tells your script when an element comes near the viewport. You use it for things the attribute cannot handle, such as CSS background images, video files and custom widgets.
Use the native attribute wherever it works and keep JavaScript for the rest. The native version is simpler, cannot break if a script fails, and is supported in all current major browsers for both images and iframes.
What to lazy load, and what never to
| Element | Lazy load? | Why |
|---|---|---|
| Hero image or first-screen banner | No | Delaying it makes Largest Contentful Paint slower |
| Logo and images in the header | No | Visible straight away, and usually small |
| Images further down the page | Yes | Most visitors never scroll to all of them |
| Product grids beyond the first row | Yes | Large saving on category pages |
| YouTube, maps and other iframes below the fold | Yes | Each embed can pull in hundreds of kilobytes of script |
| Video files | Use preload and a poster | There is no loading attribute for <video> |
| Text content | No | Search engines and readers need it in the HTML |
The rule that matters most: nothing in the first screenful should be lazy loaded, on mobile or desktop. Many themes and plugins add loading="lazy" to every image, and the most common lazy loading mistake on real sites is a lazy hero image. The LCP guide explains why that slows the page.
Lazy loading in HTML with loading=lazy
For an image below the fold, add the attribute and always give the width and height, so the browser reserves the space before the file arrives:
<img src="/images/oven-600.webp"
srcset="/images/oven-600.webp 600w, /images/oven-1200.webp 1200w"
sizes="(max-width: 700px) 100vw, 700px"
width="1200" height="800"
alt="Bread loaves cooling beside the bakery oven"
loading="lazy" decoding="async">
For an embed:
<iframe src="https://www.youtube-nocookie.com/embed/VIDEO_ID"
width="560" height="315" title="How we shape sourdough"
loading="lazy" allowfullscreen></iframe>
The browser does not wait until the image is exactly on screen. Chrome starts loading lazy images when they are within roughly 1,250 pixels of the viewport on a fast connection and further away on slow connections, so in normal scrolling the image is ready before the reader reaches it.
How the attribute behaves
loading="eager"or no attribute means load straight away. You do not need to writeeageron the hero; just leave the attribute off and addfetchpriority="high"to the main image.- Without
widthandheight, or a CSSaspect-ratio, the image takes up no space until it loads, and the content below jumps. See image dimension fixes. - Browsers only defer lazy images when JavaScript is enabled, as a privacy measure. With JavaScript off the image simply loads normally, so nothing breaks.
- An image inside an element hidden with
display: nonewill not load until it is shown.
Lazy loading in JavaScript with IntersectionObserver
When you need lazy loading for something the attribute cannot cover, keep the real address in a data- attribute and copy it across when the element approaches the viewport:
<img data-lazy src="/images/placeholder.svg"
data-src="/images/team-800.webp" width="800" height="533"
alt="The Example Bakery team at the counter">
<noscript><img src="/images/team-800.webp" width="800" height="533"
alt="The Example Bakery team at the counter"></noscript>
<script>
document.addEventListener('DOMContentLoaded', () => {
const items = document.querySelectorAll('[data-lazy]');
const show = (el) => {
if (el.dataset.src) el.src = el.dataset.src;
if (el.dataset.srcset) el.srcset = el.dataset.srcset;
if (el.dataset.bg) el.style.backgroundImage = 'url(' + el.dataset.bg + ')';
el.removeAttribute('data-lazy');
};
if (!('IntersectionObserver' in window)) { items.forEach(show); return; }
const io = new IntersectionObserver((entries) => {
entries.forEach((e) => {
if (e.isIntersecting) { show(e.target); io.unobserve(e.target); }
});
}, { rootMargin: '300px 0px' });
items.forEach((el) => io.observe(el));
});
</script>
The rootMargin of 300 pixels starts the download a little before the element is visible. The fallback loads everything at once in the rare browser without the API, and the <noscript> copy covers visitors with JavaScript switched off.
Background images and video
The same script handles a section with a large CSS background: give it a data-lazy and a data-bg attribute, and the style is only set when it comes near the screen. For video, write preload="none" and a small poster image, so nothing but the poster downloads until the visitor presses play.
Platform notes
- WordPress. Core adds
loading="lazy"to content images and iframes automatically and, since version 5.9 (refined in 6.3), tries to skip the image most likely to be the hero. Page builders, sliders and optimisation plugins can still lazy load the first image. Check the rendered HTML. If you run a plugin for lazy loading as well, turn one of the two off. See loading lazy not working. - Shopify. The Liquid
image_tagfilter accepts aloadingparameter. Many themes setloading: "lazy"on every section, so edit the first section on each template to load eagerly. - Next.js. The
next/imagecomponent lazy loads by default. Mark the hero with thepriorityprop, orpreloadin newer versions, so it loads eagerly. - React and other frameworks. Write
loading="lazy"straight on the<img>element. For components loaded on demand, such as a heavy chart, use the framework's code splitting so the script itself is only fetched when needed.
Worked example
Example Bakery has a recipe page with a hero photo, twelve step photos and a YouTube video at the bottom. Before the change, all thirteen photos and the video player downloaded on load, about 3.1 MB on a phone.
- The hero keeps no
loadingattribute and gainsfetchpriority="high". - The first step photo, which sits just under the hero on a phone, is also left eager.
- The other eleven photos get
loading="lazy", plus their width and height. - The video iframe gets
loading="lazy"and atitle.
After the change, the first-screen download drops to about 520 KB, the hero appears sooner, and the remaining photos arrive as the reader scrolls. Layout shift stays at zero because every image has dimensions. You can confirm the pattern with the steps in how to check lazy loading is working.
The fix, step by step
Step 1
List what is in the first screen
Open the page on a phone-sized viewport in Chrome DevTools and note every image, video and embed visible before you scroll, then repeat on a desktop width. These must load eagerly. Everything below that line is a candidate for lazy loading. Write the list down per template, such as home page, blog post and product page, because the first screen usually changes between templates and devices.
Step 2
Remove lazy loading from the first screen
Search the rendered HTML for loading="lazy" on the items in your first-screen list and remove it from the template, theme setting or plugin that adds it. Add fetchpriority="high" to the single main image. Reload with the Network panel open and check that the hero request starts near the top of the waterfall, before images further down the page are requested at all, and that the hero now shows high priority in the Priority column.
Step 3
Add loading=lazy to images below the fold
For each image below the first screen, add loading="lazy" and make sure it has width and height attributes that match its aspect ratio. Keep a real src and srcset on the tag so no script is needed. Reload the page without scrolling and check in the Network panel that the lower images have not been requested, then scroll down slowly and watch each one load just before it reaches the screen.
Step 4
Lazy load iframes and embeds
Add loading="lazy" to YouTube, Vimeo, map and form iframes that sit below the fold, and give each a title that describes it for screen reader users. For a heavy embed near the top of the page, consider a facade: a static image with a play button that swaps in the real player when clicked. Confirm in the Network panel that the embed's scripts only load once you scroll down.
Step 5
Use IntersectionObserver for the rest
For CSS background images, custom widgets or content that the attribute cannot cover, move the address into a data-src or data-bg attribute and use an IntersectionObserver with a rootMargin of a few hundred pixels to set it when the element nears the viewport. Add a fallback that loads everything when the API is missing, and a noscript copy for important images. Test by scrolling slowly and quickly.
Step 6
Handle video separately
Video elements have no loading attribute, so set preload="none" and a small, compressed poster image on each one below the fold. For autoplaying background videos, load the source with your observer only when the section comes into view, and pause it when it leaves. Check the Network panel to confirm the video file itself is not downloaded until the visitor plays it or scrolls to it.
Step 7
Check crawlers can see the content
Run the page through the URL Inspection live test in Google Search Console and open the rendered HTML. Every lazy image should have its real address in src, not only in a data attribute, and no content should depend on a scroll event. Also view the page with JavaScript switched off in DevTools settings to confirm that the noscript fallbacks and native lazy images still show.
Step 8
Measure before and after
Run a lab speed test on mobile and compare total page weight, LCP and layout shift with your earlier figures. The first-screen download should be smaller, LCP the same or better, and layout shift unchanged at or near zero. Repeat on each template you changed. If LCP got worse, an image in the first screen is still lazy loaded, so return to the second step and check again.
Edge cases worth knowing
- Carousels and tabs. Images in hidden slides or tabs wait until shown. Leave the first slide eager and let the rest lazy load.
- Printing. In some browsers, images that never came near the screen can be missing when someone prints a long page. If printing matters, such as for recipes, test it.
- Anchor links. A visitor jumping to a section far down the page can see blank space briefly while images load. Dimensions keep the layout steady.
- Old JavaScript libraries. Some older lazy load libraries only work on scroll and keep the image in
data-srcwith no fallback. Replace them with the native attribute. - Above the fold changes by device. An image below the fold on desktop may be the first thing on a phone. Test both layouts.
Keeping lazy loading correct
- Make the first section of each template eager in the template itself, so editors adding a new hero cannot turn lazy loading back on.
- Run one lazy loading method. Overlapping plugin and theme options cause images that never appear.
- Check new pages with the URL Inspection live test in Search Console and look at the rendered HTML: lazy images should have a real
src. - Keep an eye on LCP and layout shift in your speed checks after each theme or plugin update. The page speed guide covers the other common slowdowns.
1. What is lazy loading?
Lazy loading is a technique where a web page delays downloading images, videos, iframes or other resources until they are about to come into view, instead of loading everything when the page opens. It saves data for visitors who never scroll to the bottom, reduces the competition for bandwidth while the first screen loads, and helps pages appear faster on slow phone connections. In web development it is done with the HTML loading attribute or with JavaScript, usually through the IntersectionObserver API.
2. How do I implement lazy loading in HTML?
To implement lazy loading in HTML, add loading set to lazy on each img or iframe element that sits below the first screen, and give every image its width and height. That is all the code needed, and all current major browsers support it. The browser then decides when to fetch each file, usually well before the visitor scrolls to it. Leave the attribute off the hero image and anything else visible when the page opens, because those should load straight away.
3. How do I implement lazy loading in JavaScript?
To implement lazy loading in JavaScript, store the real image address in a data attribute, then create an IntersectionObserver that watches those elements and copies the address into src when each one comes within a few hundred pixels of the viewport. Stop observing each element once it has loaded. Add a fallback that loads everything if the API is missing and a noscript copy for key images. Use this for background images, video and widgets, and the native attribute for normal images.
4. Is lazy loading bad for SEO?
Lazy loading is not bad for SEO when it is done correctly, and Google recommends it for images further down long pages. Problems arise when the hero image is lazy loaded, which slows Largest Contentful Paint, or when content only loads after a scroll or click, because Googlebot does not scroll. Native loading set to lazy and IntersectionObserver both work with Google rendering. Check with the URL Inspection live test that the rendered HTML contains your images and text.
5. Should I lazy load images above the fold?
No, you should not lazy load images above the fold. Lazy loading tells the browser to wait until it knows where the image sits on the page, which delays the request for the very images visitors see first. That makes Largest Contentful Paint slower. Load first-screen images normally by leaving the loading attribute off, and give the main image fetchpriority set to high. Check the fold on a phone as well as a desktop, because it falls in a different place.
6. Does lazy loading work for background images?
Native lazy loading does not work for CSS background images, because the loading attribute only applies to img and iframe elements. To lazy load a background, keep the image address in a data attribute on the element and use an IntersectionObserver to set the background style when the section comes near the viewport. Alternatively, where the image carries meaning, replace the background with a normal img element positioned behind the content, which can then use the attribute directly.
7. Why are my lazy loaded images not showing?
Lazy loaded images that never appear usually have their real address only in a data attribute and a script that failed, was deferred too long or only listens for scroll events. Other causes are two lazy loading tools fighting over the same images, images inside hidden containers, and a caching plugin that strips the script. Check the browser console for errors and the rendered HTML for a real src. Switching to the native loading attribute removes most of these failure points.
The page speed audit on aiwebpageseo.com shows your LCP element and the images on the page, so you can see at a glance whether the hero is lazy loaded or images further down are loading eagerly.
Open Page Speed Fixes โ