Most pages should lazy load only images below the fold – and keep the hero image eager. If you lazy load the main above-the-fold image, you can slow LCP instead of helping it.
Here’s the short version:
- I use
loading="lazy"for images people won’t see right away - I keep the hero or LCP image on
loading="eager" - I add
fetchpriority="high"to the hero image - I set
widthandheighton every image to help keep CLS at 0.1 or lower - I use
srcsetandsizesso each screen gets the right file - I test in Lighthouse, PageSpeed Insights, and DevTools before publishing
A few numbers matter here: a good LCP is 2.5 seconds or less at the 75th percentile, and images are the LCP element on 85% of desktop pages and 76% of mobile pages. That’s why I never lazy load the image most people see first.
If native HTML is enough, I use it. If I need more control, I use IntersectionObserver with a small preload buffer like 200px.
| Method | Best for | Main tradeoff |
|---|---|---|
loading="lazy" |
Standard below-the-fold images | Less control |
loading="eager" + fetchpriority="high" |
Hero and LCP images | Loads right away |
IntersectionObserver |
Custom triggers and background images | More code to maintain |
The goal is simple: defer offscreen images, protect LCP, and prevent layout shift.
Prepare Images Before Adding Lazy Loading
Lazy loading helps only after images are sized and served the right way. If an image is too big, missing dimensions, or has no alt text, adding loading="lazy" doesn’t fix the root issue. It just pushes that issue a little later.
Set Dimensions, Alt Text, and File Formats
Always define width and height on every image, or set a defined aspect ratio. That gives the browser space to reserve before the image loads and helps prevent Cumulative Layout Shift (CLS). When dimensions are missing, the image box can collapse, trigger CLS, and interfere with lazy-loading behavior.
Write descriptive alt text for informative images. For decorative images, use empty alt.
For file formats, WebP should be your default for most site images. It can cut file size by 25–35% compared with JPEG and 35–50% compared with PNG. If your workflow supports it, AVIF can shrink files even more and produce images as small as one-third to one-fourth the size of older formats without noticeable quality loss. A simple rule works well here:
- Use WebP by default
- Use AVIF where supported
- Use JPEG for photos
- Use PNG only when you need transparency or sharp edges
Use srcset and sizes to send the right image size to each device. Try to keep each image under 200 KB, and use an automated CMS pipeline to create multiple resolutions.
Once image sizes and formats are in good shape, the next step is deciding which assets should load right away and which can wait.
Decide Which Images to Defer in Your CMS or Templates
Review templates one by one. Don’t turn on lazy loading across the whole site and call it done. Use lazy loading only for images below the fold.
Many modern WordPress setups support native lazy loading by default, so check that your theme or page builder isn’t applying it to key images like the first image in a template or a homepage hero. For those images, use fetchpriority="high" instead. It’s smart to build that rule into the template itself so hero images stay out of lazy loading.
After you exclude critical images, implement lazy loading in the markup.
sbb-itb-7a4ada9
How to Implement Native HTML Lazy Loading
Native HTML lazy loading is about as simple as it gets. You add one attribute, and modern browsers do the heavy lifting for you, making it a simple part of your search engine optimization strategy.
Add loading="lazy" to Offscreen Images
Once you’ve ruled out critical images, add loading="lazy" to the rest of the images that sit below the fold. For a hero image, or anything people see as soon as the page opens, leave that attribute off or use loading="eager" with fetchpriority="high".
Here’s what that looks like:
<!-- Hero image: loads immediately, signals high priority --> <img src="hero.webp" fetchpriority="high" loading="eager" width="1200" height="600" alt="Team working at a desk"> <!-- Below-the-fold image: deferred until near the viewport --> <img src="team-photo.webp" loading="lazy" decoding="async" width="800" height="450" alt="Office team meeting">
The decoding="async" attribute tells the browser it can keep parsing the rest of the page while the image gets decoded. It’s a small touch, but it helps keep things moving.
Also, keep width and height on every image. That way, the browser sets aside the right amount of space up front, and lazy loading won’t cause layout shifts. Nobody likes a page that jumps around.
Use Responsive Image Markup With Lazy Loading
Native lazy loading works nicely with srcset and sizes. The browser picks the right image file for the screen and waits to load it until the image is needed. So you get deferred loading and lower bandwidth use from the same block of markup.
<img srcset="photo-400.webp 400w, photo-800.webp 800w, photo-1200.webp 1200w" sizes="(max-width: 600px) 400px, (max-width: 1024px) 800px, 1200px" src="photo-800.webp" loading="lazy" decoding="async" width="800" height="500" alt="Product display on a white background">
This pattern fits below-the-fold responsive images well. If you want more control, or you need older browser support, that’s when a JavaScript loader starts to make sense.
Native HTML vs. JavaScript: A Side-by-Side Comparison
The choice between native HTML and JavaScript lazy loading mostly comes down to control.
If you want the easiest setup, native HTML is the clear winner. If you need custom loading thresholds or older browser support, JavaScript can earn its keep.
| Feature | Native HTML (loading="lazy") |
JavaScript (Intersection Observer) |
|---|---|---|
| Implementation Effort | Minimal – single attribute | Moderate – requires script and data-src attributes |
| Browser Support | Supported in modern browsers | Broad – add a polyfill for older browsers |
| Control | Browser-defined thresholds | Fine-grained – custom offsets and callbacks |
| Maintenance Overhead | Zero – native to HTML | Requires script updates and monitoring |
| Main Thread Impact | None – handled by browser | Low to moderate – script execution adds overhead |
Use native HTML when the job is simple. Use Intersection Observer when you need custom thresholds.
JavaScript Lazy Loading for More Control

Native HTML vs JavaScript Lazy Loading: Side-by-Side Comparison
When native lazy loading feels a bit too narrow, JavaScript gives you tighter control to transform your website performance. It makes sense when you need custom trigger timing, when you want to lazy load CSS background images, or when you need a fallback for older browsers.
Use Intersection Observer With data-src and data-srcset
For offscreen images and background assets, IntersectionObserver lets you decide exactly when files should load. The usual pattern is simple: keep the real image URLs in data-src and data-srcset, then move them into src and srcset as the image gets close to the viewport.
const images = document.querySelectorAll('img.lazyload'); const observer = new IntersectionObserver((entries, obs) => { entries.forEach(entry => { if (!entry.isIntersecting) return; const img = entry.target; img.src = img.dataset.src; if (img.dataset.srcset) { img.srcset = img.dataset.srcset; } img.classList.remove('lazyload'); obs.unobserve(img); }); }, { rootMargin: '200px 0px', threshold: 0 }); images.forEach(img => observer.observe(img));
Using rootMargin: '200px 0px' starts loading before the image actually appears on screen. That small buffer helps images show up on time instead of lagging behind the scroll. And once an image loads, obs.unobserve(img) stops tracking it, which cuts extra browser work.
Keep the script lean. It should do one job well: load the images it needs, when it needs them.
Add a Fallback and Keep the Script Lightweight
Not every browser environment supports IntersectionObserver, so it helps to add a simple check:
if ('IntersectionObserver' in window) { // Run the observer logic above } else { // Fallback: load all images immediately images.forEach(img => { img.src = img.dataset.src; if (img.dataset.srcset) img.srcset = img.dataset.srcset; }); }
If IntersectionObserver isn’t available, load all images right away. It’s not fancy, but it gets the job done and makes sure those browsers still display images.
When a Helper Is Worth It
Use a helper only if your project already relies on one. In most cases, a small custom IntersectionObserver script is enough. A library or framework helper makes sense only when it’s already part of the stack and covers cases your own script doesn’t.
Test and Tune Your Lazy Loading Setup
Check LCP, CLS, and Offscreen Image Behavior
After implementation, test lazy loading before it goes live. Whether you used native HTML or JavaScript, check the result in the browser.
A "Good" LCP score is ≤ 2.5 seconds at the 75th percentile (p75) of real-user loads, and a "Good" CLS score is 0.1 or less.
Use the Chrome DevTools Network tab to make sure offscreen images begin loading only when they get close to the viewport. That’s how you confirm the browser is deferring only the images meant to wait. If a below-the-fold image loads as soon as the page opens, something is set up wrong.
Test mobile and desktop separately. A page can look fine on desktop and still struggle on mobile, where slower CPUs and weaker connections can delay lazy-loaded images. Run PageSpeed Insights, Lighthouse, and GTmetrix to spot regressions before they stack up.
Practical Tuning Rules to Follow
Use these checks to catch the most common mistakes.
| Rule | Why It Matters |
|---|---|
Keep the hero/LCP image eager and set fetchpriority="high" |
Lazy loading the hero can increase total page load time by 20% to 30%; fetchpriority="high" can save 200–600 ms |
| Confirm every image reserves space before load | Without dimensions, the browser reserves no space until the image loads, causing layout shifts |
| Lazy load everything below the fold | Reduces initial page weight without affecting what the user sees first |
Don’t apply lazy loading to every image by default. That broad approach can delay hero images and other above-the-fold content that should load right away.
Conclusion: A Clear Process for Safer Image Deferral
In practice, the process is simple: understand what lazy loading defers, choose the right images to delay, start with native HTML loading="lazy", and check the outcome with real performance data. Then tune only the images that should wait.
FAQs
Which images should not be lazy loaded?
Don’t lazy load your Largest Contentful Paint (LCP) image. In most cases, it shows up above the fold, so delaying it can make the page feel slower and hurt Core Web Vitals.
Lazy load only images below the fold. For critical hero images, use loading="eager" or leave the attribute off. You can also add fetchpriority="high" to help the browser load that image sooner.
When should I use IntersectionObserver?
Use IntersectionObserver for images below the fold. It lets you wait to load an image until it enters the user’s viewport, which helps keep the main thread responsive and can improve overall site performance.
But don’t use it for LCP or hero images. If you delay those above-the-fold assets, your page can feel slower and your Core Web Vitals can take a hit.
How do I know if lazy loading is hurting LCP?
Lazy loading can hurt LCP when it makes the browser wait to download an image until it has confirmed that the image is in the viewport.
Check your LCP element in Chrome DevTools or PageSpeed Insights. You can also use a PerformanceObserver warning.
If the LCP image is lazy-loaded, remove loading="lazy" and consider using fetchpriority="high" instead.



