Lazy Loading Done Right: Images, Components, and Routes
Lazy loading is one of the highest-ROI performance wins. Learn the correct patterns for images, React components, and routes — and the mistakes that hurt more than they help.
The Three Types of Lazy Loading
Lazy loading means deferring the loading of resources until they're actually needed. In web development, this applies to three distinct areas: images (network bytes), JavaScript components (parse/compile time), and routes (entire page bundles). Each has different techniques and tradeoffs.
Image Lazy Loading
The native loading=lazy attribute is supported in all modern browsers and should be your default for below-fold images:
<!-- Always eager-load above-fold images -->
<img src="hero.jpg" alt="Hero" loading="eager" />
<!-- Lazy-load everything else -->
<img src="product.jpg" alt="Product" loading="lazy" width="400" height="300" />
Critical mistake to avoid: lazy-loading your LCP image. The browser can't start loading it until it's visible, which kills your LCP score. Always use loading=eager (or just omit the attribute) for your hero image.
Also always provide width and height attributes — without them, the browser can't reserve space and you'll get layout shift (CLS) when images load.
React Component Lazy Loading
Use React.lazy() with Suspense to split heavy components into separate chunks:
import { lazy, Suspense, useState } from 'react';
// These create separate JS chunks, loaded on demand
const RichTextEditor = lazy(() => import('./RichTextEditor'));
const DataChart = lazy(() => import('./DataChart'));
const VideoPlayer = lazy(() => import('./VideoPlayer'));
function ArticleEditor({ showChart }) {
const [editorVisible, setEditorVisible] = useState(false);
return (
<div>
<button onClick={() => setEditorVisible(true)}>
Open Editor
</button>
{editorVisible && (
<Suspense fallback={<div className="skeleton h-64" />}>
<RichTextEditor />
</Suspense>
)}
{showChart && (
<Suspense fallback={<div className="skeleton h-48" />}>
<DataChart />
</Suspense>
)}
</div>
);
}
Preloading on Hover
You can start loading a component before the user clicks by triggering the import on hover — this eliminates perceived loading time:
const Modal = lazy(() => import('./HeavyModal'));
// Preload the chunk when user hovers over the button
const preloadModal = () => import('./HeavyModal');
function App() {
const [open, setOpen] = useState(false);
return (
<>
<button
onMouseEnter={preloadModal} // preload on hover
onClick={() => setOpen(true)}
>
Open Settings
</button>
{open && (
<Suspense fallback={null}>
<Modal onClose={() => setOpen(false)} />
</Suspense>
)}
</>
);
}
IntersectionObserver for Scroll-Based Loading
For content below the fold, use IntersectionObserver to load when approaching the viewport:
import { useEffect, useRef, useState } from 'react';
function LazySection({ children }) {
const ref = useRef();
const [visible, setVisible] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setVisible(true);
observer.disconnect();
}
},
{ rootMargin: '200px' } // start loading 200px before visible
);
observer.observe(ref.current);
return () => observer.disconnect();
}, []);
return <div ref={ref}>{visible ? children : <Skeleton />}</div>;
}
Common Mistakes
- Lazy-loading above-fold images (kills LCP)
- Missing width/height on images (causes CLS)
- No Suspense boundary around lazy components (crashes)
- Lazy-loading tiny components — only worth it above ~20kb
Applied correctly, lazy loading is one of the few optimizations that simultaneously improves LCP, FID/INP, and bandwidth usage.
Admin
Cal.com
Open source scheduling — tự host booking system, thay thế Calendly. Free & privacy-first.
Bình luận (0)
Đăng nhập để bình luận
Chưa có bình luận nào. Hãy là người đầu tiên!