Native loading Attribute — Using loading=lazy for Images and Iframes
In this tutorial, you will learn about Native loading Attribute. We cover key concepts, practical examples, and best practices to help you master this topic.
The native loading attribute lets browsers defer offscreen images and iframes without JavaScript, improving Core Web Vitals with zero library dependencies.
What You'll Learn
By the end of this tutorial, you'll understand how the native loading attribute works, how to use it for images and iframes, how to prioritize critical resources with eager loading, and how to handle browsers without support.
Why It Matters
Native Lazy Loading requires zero JavaScript, zero libraries, and zero configuration. Adding a single HTML attribute defers non-critical resources and improves performance — the simplest optimization you can make.
Real-World Use
A documentation site with 50 screenshots adds loading=lazy to all images below the fold. Initial page weight drops from 1.8MB to 0.3MB. The change was a single find-and-replace in the template. No JavaScript, no libraries.
Native Loading Behavior
graph TD
A[Browser parses
loading attribute] --> B{loading value?}
B -->|eager| C[Load image
immediately]
B -->|'lazy'| D{Image within
viewport?}
D -->|Yes| C
D -->|No| E[Image is
offscreen]
E --> F[Browser tracks
scroll position]
F --> G{Image approaching
viewport?}
G -->|No| H[Keep deferred]
G -->|Yes| I[Load image
~300-500px before viewport]
I --> J[Image appears
fully loaded]
style C fill:#27ae60,color:#fff
style E fill:#f39c12,color:#fff
style I fill:#4a90d9,color:#fff
style J fill:#27ae60,color:#fff
Image Lazy Loading
<!-- Standard lazy loaded image -->
<img src="photo.jpg" alt="Description" loading="lazy" width="800" height="600">
<!-- With responsive images -->
<img src="photo-400.jpg"
srcset="photo-400.jpg 400w, photo-800.jpg 800w, photo-1200.jpg 1200w"
sizes="(max-width: 600px) 400px, (max-width: 1000px) 800px, 1200px"
alt="Responsive lazy image"
loading="lazy"
width="1200" height="800">
<!-- Critical hero image — never lazy -->
<img src="hero.jpg"
alt="Hero banner"
loading="eager"
fetchpriority="high"
width="1600" height="900">
<!-- Progressive enhancement with data-src fallback -->
<img src="placeholder.jpg"
data-src="actual-photo.jpg"
alt="Progressive enhancement"
loading="lazy"
width="800" height="600"
onerror="this.src=this.dataset.src">
Iframe Lazy Loading
<!-- Lazy loaded YouTube embed -->
<iframe src="https://www.youtube.com/embed/dQw4w9WgXcQ"
title="YouTube video"
loading="lazy"
width="560" height="315"
allowfullscreen>
</iframe>
<!-- Lazy loaded analytics widget -->
<iframe src="https://widget.analytics.com/dashboard"
loading="lazy"
width="100%" height="400"
title="Analytics Dashboard">
</iframe>
<!-- Lazy loaded social media embed -->
<iframe src="https://platform.twitter.com/widgets/tweet.html"
loading="lazy"
width="550" height="300"
title="Tweet embed">
</iframe>
<!-- Critical iframe (above fold) — load eagerly -->
<iframe src="https://maps.google.com/maps?..."
title="Store location map"
loading="eager"
width="100%" height="400">
</iframe>
Browser Detection and Fallback
// Fallback for browsers without native lazy loading
document.addEventListener('DOMContentLoaded', function () {
// Check if native lazy loading is supported
const supportsLazy = 'loading' in HTMLImageElement.prototype;
if (!supportsLazy) {
console.log('Native lazy loading not supported, using Intersection Observer fallback');
// Intersection Observer fallback
const lazyImages = document.querySelectorAll('img[loading="lazy"]');
const lazyIframes = document.querySelectorAll('iframe[loading="lazy"]');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const element = entry.target;
if (element.tagName === 'IMG') {
element.src = element.src || element.dataset.src;
} else if (element.tagName === 'IFRAME') {
element.src = element.src || element.dataset.src;
}
element.removeAttribute('loading');
observer.unobserve(element);
}
});
}, {
rootMargin: '200px 0px',
threshold: 0.01
});
lazyImages.forEach(img => observer.observe(img));
lazyIframes.forEach(iframe => observer.observe(iframe));
}
});
Performance Monitoring
// Monitor lazy loading effectiveness
class LazyLoadingMonitor {
constructor() {
this.lazyElements = new Map();
this.metrics = {
total: 0,
loaded: 0,
avoidedBytes: 0
};
}
track(element) {
const id = `lazy-${this.metrics.total++}`;
element.dataset.lazyId = id;
this.lazyElements.set(id, {
element,
loaded: false,
size: null
});
// Estimate size from element attributes
const width = parseInt(element.getAttribute('width')) || 0;
const height = parseInt(element.getAttribute('height')) || 0;
const estimatedBytes = (width * height * 3) / 10; // rough JPEG estimate
this.metrics.avoidedBytes += estimatedBytes;
}
markLoaded(element) {
const id = element.dataset.lazyId;
if (id && this.lazyElements.has(id)) {
const entry = this.lazyElements.get(id);
entry.loaded = true;
this.metrics.loaded++;
}
}
report() {
const percentAvoided = ((this.metrics.avoidedBytes / (1024 * 1024))).toFixed(1);
console.log(`Lazy Loading Report:`);
console.log(` Total lazy elements: ${this.metrics.total}`);
console.log(` Loaded so far: ${this.metrics.loaded}`);
console.log(` Estimated bandwidth avoided: ${percentAvoided} MB`);
}
}
const monitor = new LazyLoadingMonitor();
Common Mistakes
- Using loading=lazy on the LCP image. The largest contentful paint image should load eagerly. Lazy loading it delays LCP and hurts performance scores.
- Forgetting dimensions on lazy images. Images without width/height cause Cumulative Layout Shift when they load. Always set dimensions.
- Lazy loading images that are always visible. Navigation logos, user avatars, and sidebar icons are always visible. Don't lazy load them.
- Using loading=lazy on print stylesheets. Print versions need all images. Use @media print to override lazy loading.
- Not testing on slow networks. Native lazy loading works, but on 3G, images may load too late. Test with network throttling.
Practice Questions
- What values does the loading attribute accept?
- How does the browser decide when to load a lazy image?
- Why should the LCP image never use loading=lazy?
- How do you provide a fallback for browsers without native lazy loading?
- What is the difference between loading and fetchpriority attributes?
Challenge: Create a comparison page that benchmarks native lazy loading vs eager loading: 30 images loaded with each approach, measure page weight, LCP, and load time using Performance API, and present results in a chart.
FAQ
Mini Project
Build a page that demonstrates native lazy loading with monitoring: 25 images with loading=lazy, 5 critical images with loading=eager, a real-time counter showing loaded/deferred images, a bandwidth savings estimator, and a benchmark comparison table.
What's Next
You've mastered native lazy loading. Now learn about Intersection Observer for custom lazy loading implementations.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro