Lazy loading images: what to lazy-load and what never to
Lazy loading images cuts bytes without hurting speed. The rule is simple: never lazy-load anything above the fold, always lazy-load the rest. Here is how to do it with images, iframes and backgrounds, and how to avoid the hero image trap that ruins LCP.
By Théophile Louvart, founder of Porteur · Updated 14 September 2026 · Markdown
What to lazy-load and what never to
Lazy-load every image that starts below the fold. Never lazy-load anything visible at first paint.
- Do not lazy-load your hero image on "/" or "/pricing".
- Do lazy-load thumbnails below the first viewport on "/blog".
- Do lazy-load product gallery images that sit under the fold on "/product/widget".
- Do not lazy-load the logo, nav icons or any image in the header.
- Do lazy-load user avatars that appear down the page on "/community".
This rule protects Largest Contentful Paint. LCP counts the largest image, video poster, background image or text in the first viewport. A lazy hero delays it.
A fixed page looks like this: the hero image on "/" has no lazy flag and uses fetchpriority=high. Every image below the fold has loading=lazy.
Native loading: how browsers handle it
Use the native attribute on images and iframes. Browsers support it across the board as of 2026. They start loading a little before the viewport, not exactly at the edge.
<img src="/images/guide-card.jpg" alt="Guide card" loading="lazy" width="640" height="360">Do not overthink the threshold. The browser has more context than you. It knows the connection, device and scroll speed. Let it decide when to fetch.
Always set width and height or aspect-ratio. That prevents layout shifts when the image arrives. CLS should stay under 0.1 for a good score.
Add decoding=async on images that do not block the first paint. The browser can decode them off the critical path.
<img src="/images/review-avatars.jpg" alt="Customer avatars" loading="lazy" decoding="async" width="320" height="320">Avoid the LCP trap: the hero image
A lazy hero image ruins LCP. The browser will not fetch it early, so the largest element renders late. Keep LCP under 2.5 s for a good field score.
Remove lazy on the hero
On "/" replace loading="lazy" with nothing. Keep width and height.
Give it priority
Add fetchpriority=high. This hints the browser to fetch it sooner.
Use responsive sources
Serve WebP or AVIF with srcset and sizes so phones do not download desktop sizes.
Preload when it is CSS
If the hero is a background-image, preload the file. Then keep it out of lazy logic.
Render the HTML server-side
Name the hero image in the HTML. Do not insert it later with JavaScript.
<!-- Good hero on / -->
<link rel="preload" as="image" href="/images/hero-1280.webp" imagesrcset="/images/hero-640.webp 640w, /images/hero-1280.webp 1280w" imagesizes="100vw">
<img src="/images/hero-1280.webp"
srcset="/images/hero-640.webp 640w, /images/hero-1280.webp 1280w"
sizes="100vw"
alt="Plan your roadmap"
width="1280" height="720"
fetchpriority="high">
A fixed page shows LCP as that hero image, fetched with high priority, not lazy. Other images on "/" below the fold keep loading=lazy and decoding=async.
Iframes: embeds, videos and maps
Lazy-load iframes that appear below the fold. The native attribute works here too. Embeds are heavy, so this saves a lot of work on first paint.
<!-- YouTube embed below the fold on /guides/getting-started -->
<iframe
loading="lazy"
width="560" height="315"
src="https://www.youtube.com/embed/VIDEO_ID"
title="Product demo"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen>
</iframe>Above the fold, replace the iframe with a static facade and load the iframe on interaction. This reduces third-party script cost and improves INP and TBT in lab runs.
<!-- Facade pattern for a hero video on /pricing -->
<button id="play-demo" aria-label="Play demo">
<img src="/images/demo-poster.webp" width="800" height="450" alt="Watch the demo">
<span>Play</span>
</button>
<script type="module">
document.getElementById('play-demo').addEventListener('click', () => {
const iframe = document.createElement('iframe');
iframe.width = 800; iframe.height = 450;
iframe.src = 'https://www.youtube.com/embed/VIDEO_ID?autoplay=1';
iframe.title = 'Product demo';
iframe.allow = 'autoplay; encrypted-media; picture-in-picture';
iframe.referrerPolicy = 'strict-origin-when-cross-origin';
document.getElementById('play-demo').replaceWith(iframe);
});
</script>A fixed page uses loading=lazy on a below-the-fold map iframe on "/contact". The hero has a static image until you click Play. INP stays under 200 ms for clicks that do not mount the iframe.
Background images and JavaScript-inserted content
CSS backgrounds do not support loading=lazy. The browser must discover them from CSS, which often happens late. If the image is below the fold, load it with IntersectionObserver.
<!-- Mark elements that need a background only when near viewport -->
<section class="feature" data-bg="/images/charts.webp"></section>
<style>
.feature { min-height: 480px; background-color: #f6f7f9; }
.feature.bg-loaded { background-image: url('/images/charts.webp'); background-size: cover; background-position: center; }
</style>
<script type="module">
const io = new IntersectionObserver(entries => {
for (const e of entries) if (e.isIntersecting) {
const url = e.target.dataset.bg;
if (url) {
const img = new Image();
img.decoding = 'async';
img.src = url;
img.onload = () => e.target.classList.add('bg-loaded');
e.target.removeAttribute('data-bg');
}
io.unobserve(e.target);
}
}, { rootMargin: '200px 0px' });
document.querySelectorAll('[data-bg]').forEach(el => io.observe(el));
</script>For content inserted by JavaScript, the same pattern applies. Use IntersectionObserver to load and mount when near the viewport. Avoid scroll handlers for this job.
A fixed page moves a below-the-fold hero variant that used background-image to a proper img with preload if it was above the fold. This protects LCP and keeps CLS low with width and height in HTML.
How Google sees lazy-loaded content
As of 2026, Googlebot uses a smartphone crawler. It can index lazy-loaded images and content that appear without a click or gesture. IntersectionObserver is supported by Googlebot rendering.
- Content that loads as it nears the viewport is fine.
- Content that needs a true user action is not indexed until it exists in the DOM.
- A scroll handler that only loads on scroll may not run in rendering. Use IntersectionObserver instead.
- Server-rendered HTML wins: include the key content in the document.
Check your pages with PageSpeed Insights and the URL Inspection tool. If an image is missing in the rendered HTML, Google may not index it. Do not gate it on scroll events or clicks.
Prevent layout shifts when images load
Lazy-loading often exposes weak layout. Images arrive late and push content down. Reserve space so the layout does not jump. Aim for CLS under 0.1.
- Set width and height on every img. Modern browsers use them to set the aspect ratio.
- If you cannot, set aspect-ratio in CSS on the image box.
- Give ad and embed slots a min-height so banners and iframes do not push content.
- Avoid loading bars that insert above existing content. Use overlays for notices.
- Do not animate layout properties. Use transform for motion.
<!-- Thumbnails on /blog, space reserved -->
<img
src="/images/thumb-article.webp"
alt="How we shipped faster"
loading="lazy" decoding="async"
width="384" height="216">
A fixed page keeps the grid stable when images arrive. The scroll feels calm, and the field CLS assessment passes on mobile and desktop in Search Console.
Measure the effect: field and lab data
Judge success with field data first. The Core Web Vitals assessment uses the 75th percentile of real Chrome users over 28 days. A URL passes when LCP, CLS and INP are all good.
- Good LCP is up to 2.5 s, poor above 4 s.
- Good CLS is up to 0.1, poor above 0.25.
- Good INP is up to 200 ms, poor above 500 ms.
Use PageSpeed Insights. The top section shows field data if there is enough traffic. That is what Google Search uses for the ranking signal, with content and relevance well ahead of it.
Use the lower section for lab checks. Lighthouse simulates a mid-range phone on throttled 4G. Scores vary between runs. Total Blocking Time weighs 30 percent there and is the proxy for INP in lab tests.
Expect the two to disagree. Field data wins. If a page has too few visits, the reports fall back to origin-level or show nothing. Keep testing as traffic grows.
A simple lazy loading example that works
Here is a full pattern for images below the fold on "/blog". It uses srcset, sizes, lazy loading and decoding=async, and reserves space with width and height.
<article class="post-card">
<a href="/blog/how-we-ship-faster">
<img
src="/images/thumb-ship-640.webp"
srcset="/images/thumb-ship-320.webp 320w, /images/thumb-ship-640.webp 640w, /images/thumb-ship-960.webp 960w"
sizes="(max-width: 640px) 100vw, 384px"
alt="How we ship faster"
width="384" height="216"
loading="lazy"
decoding="async">
<h2>How we ship faster</h2>
<p>What we cut to release weekly.</p>
</a>
</article>Use the same pattern in product grids. Keep the primary product image above the fold eager, with fetchpriority=high, and lazy-load the rest of the gallery below the fold on the PDP.
Common mistakes and how to fix them
- Lazy-loading the hero image. Fix: remove loading=lazy, add fetchpriority=high, and preload when it is a CSS background.
- Relying on scroll events to load content. Fix: use IntersectionObserver so Google can see it and it loads near the viewport.
- No width and height on images. Fix: set them or use aspect-ratio to stop layout shifts.
- Lazy-loading icons and logos in the header. Fix: keep critical UI eager and small. Sprite or inline tiny SVGs.
- Huge background images discovered late. Fix: preload above-the-fold backgrounds, or switch to <img> where you can.
- JavaScript inserts the image after hydration. Fix: server-render the <img> in HTML so the browser can fetch it sooner.
- Too many third-party embeds above the fold. Fix: use a facade, lazy-load the iframe, and load scripts on interaction.
- Overusing lazy on short pages. Fix: if your page fits one screen, lazy adds no value and can delay paint.
After fixes, your homepage LCP should be your hero image, fetched early, and your long pages should save requests until the user scrolls. Scrolling feels smooth and inputs stay snappy.
Questions
It means deferring image requests that start below the fold until the user is likely to see them. You add loading=lazy on those images. The browser fetches them a little before they enter the viewport so they are ready in time.
A blog list on "/blog" where only the first card is above the fold. Keep that first thumbnail eager and mark the rest with loading=lazy, decoding=async, width and height. The browser will fetch them as you scroll down.
Common causes are a lazy hero image, oversized files, no responsive srcset, a distant server, or discovery through CSS or JavaScript after load. Fix the hero first, serve WebP or AVIF with srcset and sizes, use a CDN, and put key images in the HTML.
Add loading=lazy on img elements that start below the fold and keep width and height. Pair it with decoding=async. For background images, use IntersectionObserver to set the background when the section nears view. Never lazy-load the hero.
It helps page speed, which supports the Core Web Vitals assessment. Google can index lazy-loaded content that appears without a user action. Avoid scroll-only handlers or clicks to load, because Google may not trigger them in rendering.
Yes for iframes below the fold, with loading=lazy or a facade that loads on interaction. Background images need JavaScript: use IntersectionObserver to set them when near view. Do not lazy-load above-the-fold assets.
Sources
Check my site, free
Paste your homepage URL to see which images to lazy-load, which to keep eager and where LCP is delayed, in about thirty seconds, free, with three findings you can ship.
- Free check, no card
- Read-only, your own accounts
- Readable by your agent
Read next
- GuideImage optimisation for page speed: the hero image and everything below it
- GuideLargest Contentful Paint: what slows it and what fixes it
- GuideCore Web Vitals for a product site: the three numbers
- GuideHow to read PageSpeed Insights: field data first, then the score
- GuideRender-blocking resources: what blocks the first paint and how to unblock it
- GuideThird-party scripts: the page speed you gave away
- GlossaryJavaScript SEO
- GuideAI-generated images on a product site