Bỏ qua để vào nội dung chính
Lazy Loading Done Right: The Definitive Guide to Images, Components & Routes

Lazy Loading Done Right: The Definitive Guide to Images, Components & Routes

Bởi Marcus Bennett
16 thg 3, 20266 phút đọc

A practical breakdown of every lazy loading technique available in modern React and Next.js—intersection observer, dynamic imports, route-based splitting, and native browser lazy—with performance benchmarks to show what actually moves the needle.

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.

Không spam, hủy đăng ký bất kỳ lúc nào.

Bài viết liên quan