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">
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">
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-imageproperties - 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));
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.
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)