Lazy Loading: Load Resources On-Demand
Lazy loading dramatically improves initial page load by deferring non-critical resources. Learn when and how to implement lazy loading effectively.
Quick Decision Guide
Quick Implementation Guide:
Images: Use native loading="lazy" attribute on img tags. Works in all modern browsers.
React Components: Use React.lazy() with Suspense for non-critical components: const Component = lazy(() => import('./Component'))
Custom lazy loading: Use Intersection Observer API to detect when elements enter viewport, then load content.
Third-party scripts: Defer loading of chat widgets, analytics until after initial render or on user interaction.
Best Practice: Lazy load everything below the fold. Keep above-the-fold content eager-loaded for optimal LCP (Largest Contentful Paint).
Native Image Lazy Loading
HTML loading Attribute
<!-- Lazy load (loads when near viewport) -->
<img src="image.jpg" loading="lazy" alt="Description" />
<!-- Eager load (default, loads immediately) -->
<img src="hero.jpg" loading="eager" alt="Hero" />
<!-- Auto (browser decides) -->
<img src="photo.jpg" loading="auto" alt="Photo" />How It Works
1. Browser detects images with loading="lazy"
2. Only loads images that are visible or near viewport
3. Automatically loads more as user scrolls
4. No JavaScript required!
Browser Support
✅ Chrome 77+
✅ Firefox 75+
✅ Safari 15.4+
✅ Edge 79+
React Example
function ImageGallery({ images }) {
return (
<div className="gallery">
{images.map((img, index) => (
<img
key={img.id}
src={img.url}
alt={img.alt}
loading={index < 3 ? 'eager' : 'lazy'}
width={img.width}
height={img.height}
/>
))}
</div>
);
}Pro Tip: Set loading="eager" for first 2-3 images (above-the-fold), lazy for rest.
React Component Lazy Loading
React.lazy() and Suspense
import { lazy, Suspense } from 'react';
// Lazy load component
const Comments = lazy(() => import('./Comments'));
const UserProfile = lazy(() => import('./UserProfile'));
function App() {
return (
<div>
<h1>My App</h1>
{/* Suspense provides loading fallback */}
<Suspense fallback={<div>Loading comments...</div>}>
<Comments />
</Suspense>
<Suspense fallback={<ProfileSkeleton />}>
<UserProfile />
</Suspense>
</div>
);
}Conditional Lazy Loading
function Dashboard() {
const [showChart, setShowChart] = useState(false);
return (
<div>
<button onClick={() => setShowChart(true)}>
Show Chart
</button>
{showChart && (
<Suspense fallback={<ChartSkeleton />}>
{React.createElement(lazy(() => import('./Chart')))}
</Suspense>
)}
</div>
);
}Modal Lazy Loading
const Modal = lazy(() => import('./Modal'));
function App() {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<button onClick={() => setIsOpen(true)}>Open Modal</button>
{isOpen && (
<Suspense fallback={null}>
<Modal onClose={() => setIsOpen(false)} />
</Suspense>
)}
</>
);
}Intersection Observer
Custom Lazy Loading
function lazyLoad(target) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// Element is visible
const img = entry.target;
img.src = img.dataset.src; // Load actual image
observer.unobserve(img); // Stop observing
}
});
}, {
rootMargin: '50px' // Start loading 50px before visible
});
observer.observe(target);
}
// Usage
document.querySelectorAll('img[data-src]').forEach(lazyLoad);HTML Setup
<!-- Placeholder in src, actual image in data-src -->
<img
src="placeholder.jpg"
data-src="actual-image.jpg"
alt="Description"
/>React Hook
function useIntersectionObserver(ref, callback, options) {
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
callback();
}
});
},
options
);
if (ref.current) {
observer.observe(ref.current);
}
return () => observer.disconnect();
}, [ref, callback, options]);
}
// Usage
function LazyComponent() {
const ref = useRef(null);
const [isVisible, setIsVisible] = useState(false);
useIntersectionObserver(ref, () => setIsVisible(true), {
rootMargin: '100px'
});
return (
<div ref={ref}>
{isVisible ? <HeavyContent /> : <Placeholder />}
</div>
);
}Lazy Loading Third-Party Scripts
Defer Analytics
// Load Google Analytics after page interactive
window.addEventListener('load', () => {
const script = document.createElement('script');
script.src = 'https://www.googletagmanager.com/gtag/js?id=GA_ID';
script.async = true;
document.head.appendChild(script);
});Load on Interaction
// Load chat widget on user interaction
let chatLoaded = false;
function loadChatWidget() {
if (chatLoaded) return;
const script = document.createElement('script');
script.src = 'https://cdn.chat-widget.com/widget.js';
script.async = true;
document.body.appendChild(script);
chatLoaded = true;
}
// Load on first interaction
['mousemove', 'scroll', 'keydown', 'click'].forEach(event => {
document.addEventListener(event, loadChatWidget, { once: true });
});Next.js Script Component
import Script from 'next/script';
function App() {
return (
<>
{/* Load after page is interactive */}
<Script
src="https://analytics.com/script.js"
strategy="lazyOnload"
/>
{/* Load after hydration */}
<Script
src="https://maps.googleapis.com/maps/api/js"
strategy="afterInteractive"
/>
</>
);
}Lazy Loading Strategies
When to Lazy Load
✅ Good Candidates
❌ Don't Lazy Load
Loading Strategies
1. On Visibility
// Load when element enters viewport
<IntersectionObserver>
<LazyComponent />
</IntersectionObserver>2. On Interaction
// Load when user clicks
<button onClick={() => import('./Feature')}>
Load Feature
</button>3. On Idle
// Load during browser idle time
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
import('./analytics');
});
}4. On Route
// Load when navigating to route
const Dashboard = lazy(() => import('./Dashboard'));Performance Impact
Before lazy loading:
Initial load: 2.5MB, 45 requests, 4.2s load time
After lazy loading:
Initial load: 800KB, 12 requests, 1.3s load time
Improvement: 68% smaller, 73% fewer requests, 69% fasterBest Practices
Image Lazy Loading
function OptimizedImage({ src, alt, priority = false }) {
return (
<img
src={src}
alt={alt}
loading={priority ? 'eager' : 'lazy'}
decoding="async"
// Always specify dimensions to avoid layout shift
width="800"
height="600"
/>
);
}Component Lazy Loading
// ✅ Good: Lazy load heavy components
const Chart = lazy(() => import('./Chart'));
const VideoPlayer = lazy(() => import('./VideoPlayer'));
// ❌ Bad: Lazy loading tiny components
const Button = lazy(() => import('./Button')); // Overhead > benefitError Handling
import { ErrorBoundary } from 'react-error-boundary';
function App() {
return (
<ErrorBoundary fallback={<ErrorMessage />}>
<Suspense fallback={<Loading />}>
<LazyComponent />
</Suspense>
</ErrorBoundary>
);
}Prefetching
// Prefetch on hover for instant loading
function NavigationLink({ to }) {
const handleMouseEnter = () => {
import('./pages/' + to); // Prefetch component
};
return (
<Link to={to} onMouseEnter={handleMouseEnter}>
{to}
</Link>
);
}Progressive Enhancement
<!-- Works without JavaScript -->
<img src="image.jpg" loading="lazy" alt="Description" />
<!-- Fallback for older browsers -->
<noscript>
<img src="image.jpg" alt="Description" />
</noscript>