DEV Community

Cover image for Lazy Loading Images in 2026 — When Native Is Enough and When It Isn't
YT Low
YT Low

Posted on Originally published at onlywebpro.com

Lazy Loading Images in 2026 — When Native Is Enough and When It Isn't

Full guide with live demos, blur-up placeholder code, and a complete decision table on
onlyWebPro.

One HTML attribute defers every offscreen image — no JavaScript, no library, ~95% browser coverage in 2026.

<img src="product.jpg" alt="Product" width="800" height="600" loading="lazy">
Enter fullscreen mode Exit fullscreen mode

But native lazy loading has real limitations. Here's when it's enough, and when you need Intersection Observer.


The LCP mistake that kills Core Web Vitals

This is the most common lazy loading error — never add loading="lazy" to your LCP image.

The LCP (Largest Contentful Paint) element — usually the hero image — gets delayed by 200–500ms when lazy-loaded. Lighthouse flags it directly.

<!-- ❌ Wrong — delays the hero image -->
<img src="hero.jpg" alt="Hero" loading="lazy">

<!-- ✅ Right — prioritise it instead -->
<img src="hero.jpg" alt="Hero" fetchpriority="high">
Enter fullscreen mode Exit fullscreen mode

Rule: any image visible in the first ~800px without scrolling should never be lazy-loaded.


Browser support in 2026

Browser loading="lazy"
Chrome 77+ / Edge 79+ ✓
Firefox 75+ ✓
Safari 15.4+ ✓
Global coverage ~95%

Unsupported browsers silently ignore the attribute — images load normally. No polyfill needed.


What native lazy loading cannot do

  • Custom placeholders (blur-up, skeleton, dominant colour)
  • Fade-in animations on image arrival
  • CSS background-image properties
  • Custom load threshold (distance before viewport)

For all of these, use Intersection Observer.


Intersection Observer — when you need more control

const observer = new IntersectionObserver((entries) => {
  for (const entry of entries) {
    if (!entry.isIntersecting) continue;
    const img = entry.target;
    img.src = img.dataset.src;           // swap data-src into src
    img.addEventListener('load', () => img.classList.add('loaded'));
    observer.unobserve(img);             // stop watching once loaded
  }
}, { rootMargin: '300px 0px' });        // start loading 300px early

document.querySelectorAll('img.lazy[data-src]')
  .forEach(img => observer.observe(img));
Enter fullscreen mode Exit fullscreen mode

The rootMargin: '300px 0px' gives the browser a head start — images are fully loaded before the user scrolls to them.


The quick decision table

Situation Use
Standard below-fold <img> loading="lazy"
Hero / LCP image fetchpriority="high"
Fade-in on load Intersection Observer
Blur-up placeholder Intersection Observer
CSS background-image Intersection Observer
<iframe> (YouTube, Maps) loading="lazy"

The full guide has live demos for both techniques, the complete blur-up placeholder pattern, and the CSS background-image solution.

👉 Read the full guide


Part 2 of the Frontend Performance Series on onlyWebPro.

Do you still reach for Intersection Observer or does native cover everything you need these days?

Top comments (0)