Skip to content
intermediate Phase 10 · Web Performance

Lazy Loading

Defer non-critical resources with lazy loading for images, components, and routes.

30m
0 problems
Topic Progress 0%

Image Lazy Loading

Image Lazy Loading

Defer loading of images until they enter the viewport.

Native Lazy Loading

// Simple native lazy loading
<img
  src="/image.jpg"
  alt="Description"
  loading="lazy"
  width={800}
  height={600}
/>

// With responsive images
<img
  src="/image.webp"
  srcset="/image-400.webp 400w, /image-800.webp 800w"
  sizes="(max-width: 768px) 100vw, 50vw"
  loading="lazy"
  width={800}
  height={600}
  alt="Description"
/>

Intersection Observer Implementation

// hooks/useLazyImage.js
function useLazyImage(src, options = {}) {
  const [imageSrc, setImageSrc] = useState(null);
  const [isLoaded, setIsLoaded] = useState(false);
  const imgRef = useRef(null);

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            setImageSrc(src);
            observer.disconnect();
          }
        });
      },
      { rootMargin: options.rootMargin || '100px' }
    );

    if (imgRef.current) {
      observer.observe(imgRef.current);
    }

    return () => observer.disconnect();
  }, [src, options.rootMargin]);

  const handleLoad = () => setIsLoaded(true);

  return { imgRef, imageSrc, isLoaded, handleLoad };
}

// Component
function LazyImage({ src, alt, ...props }) {
  const { imgRef, imageSrc, isLoaded, handleLoad } = useLazyImage(src);

  return (
    <div ref={imgRef} className="lazy-image-container">
      {imageSrc ? (
        <img
          src={imageSrc}
          alt={alt}
          onLoad={handleLoad}
          className={isLoaded ? 'loaded' : 'loading'}
          {...props}
        />
      ) : (
        <div className="image-placeholder" />
      )}
    </div>
  );
}

Blur-Up Technique

function BlurUpImage({ src, placeholder, alt }) {
  const [loaded, setLoaded] = useState(false);

  return (
    <div className="blur-up-container">
      <img
        src={placeholder}
        alt={alt}
        className="blur-up-placeholder"
        style={{ filter: loaded ? 'none' : 'blur(20px)' }}
      />
      {loaded && (
        <img
          src={src}
          alt={alt}
          className="blur-up-full"
          onLoad={() => setLoaded(true)}
        />
      )}
    </div>
  );
}

Component Lazy Loading

Component Lazy Loading

Load components on demand to reduce initial bundle.

React.lazy Implementation

import { lazy, Suspense } from 'react';

// Lazy load heavy components
const Chart = lazy(() => import('./Chart'));
const DataGrid = lazy(() => import('./DataGrid'));
const RichTextEditor = lazy(() => import('./RichTextEditor'));

function Dashboard() {
  const [activeTab, setActiveTab] = useState('overview');

  return (
    <div>
      <TabList>
        <Tab onClick={() => setActiveTab('overview')}>Overview</Tab>
        <Tab onClick={() => setActiveTab('charts')}>Charts</Tab>
        <Tab onClick={() => setActiveTab('data')}>Data</Tab>
      </TabList>

      <Suspense fallback={<TabLoader />}>
        {activeTab === 'overview' && <OverviewTab />}
        {activeTab === 'charts' && <Chart data={chartData} />}
        {activeTab === 'data' && <DataGrid rows={tableData} />}
      </Suspense>
    </div>
  );
}

On-Interaction Loading

function ExpandableSection({ title, loadChildren }) {
  const [isExpanded, setIsExpanded] = useState(false);
  const [Component, setComponent] = useState(null);

  const handleExpand = async () => {
    if (!Component && loadChildren) {
      const mod = await loadChildren();
      setComponent(() => mod.default);
    }
    setIsExpanded(!isExpanded);
  };

  return (
    <div>
      <button onClick={handleExpand}>
        {isExpanded ? '▼' : '▶'} {title}
      </button>
      {isExpanded && Component && (
        <div className="expandable-content">
          <Component />
        </div>
      )}
    </div>
  );
}

// Usage
<ExpandableSection
  title="Advanced Settings"
  loadChildren={() => import('./AdvancedSettings')}
/>

Conditional Loading

function App({ user }) {
  const [AdminPanel, setAdminPanel] = useState(null);

  useEffect(() => {
    if (user?.role === 'admin' && !AdminPanel) {
      import('./AdminPanel').then(module => {
        setAdminPanel(() => module.default);
      });
    }
  }, [user, AdminPanel]);

  return (
    <div>
      <Header />
      <main>
        <Routes>{/* ... */}</Routes>
      </main>
      {AdminPanel && <AdminPanel />}
    </div>
  );
}

Route Lazy Loading

Route Lazy Loading

Load route components only when navigating to them.

React Router Implementation

import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';

const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Products = lazy(() => import('./pages/Products'));
const ProductDetail = lazy(() => import('./pages/ProductDetail'));
const Cart = lazy(() => import('./pages/Cart'));
const Checkout = lazy(() => import('./pages/Checkout'));

const PageLoader = () => (
  <div className="page-loader">
    <Spinner />
    <p>Loading page...</p>
  </div>
);

function App() {
  return (
    <BrowserRouter>
      <Layout>
        <Suspense fallback={<PageLoader />}>
          <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/about" element={<About />} />
            <Route path="/products" element={<Products />} />
            <Route path="/products/:id" element={<ProductDetail />} />
            <Route path="/cart" element={<Cart />} />
            <Route path="/checkout" element={<Checkout />} />
          </Routes>
        </Suspense>
      </Layout>
    </BrowserRouter>
  );
}

Prefetching on Hover

import { Link } from 'react-router-dom';

function NavLink({ to, children }) {
  const prefetch = () => {
    // Map routes to their chunks
    const routes = {
      '/about': () => import('./pages/About'),
      '/products': () => import('./pages/Products'),
      '/cart': () => import('./pages/Cart'),
    };

    if (routes[to]) {
      routes[to]();
    }
  };

  return (
    <Link to={to} onMouseEnter={prefetch}>
      {children}
    </Link>
  );
}

Nested Routes

const DashboardLayout = lazy(() => import('./layouts/DashboardLayout'));
const Overview = lazy(() => import('./pages/dashboard/Overview'));
const Analytics = lazy(() => import('./pages/dashboard/Analytics'));
const Settings = lazy(() => import('./pages/dashboard/Settings'));

function App() {
  return (
    <Suspense fallback={<PageLoader />}>
      <Routes>
        <Route path="/dashboard" element={<DashboardLayout />}>
          <Route index element={<Overview />} />
          <Route path="analytics" element={<Analytics />} />
          <Route path="settings" element={<Settings />} />
        </Route>
      </Routes>
    </Suspense>
  );
}

Benefits of Route Lazy Loading

  • Faster initial load: Only load code for current page
  • Better caching: Smaller chunks cache better
  • Reduced memory: Unused code isn't loaded
  • Better UX: Loading indicators for navigation

Quiz

1. What is native lazy loading?

Question 1 options

2. When should you lazy load a component?

Question 2 options

3. What is the blur-up technique?

Question 3 options

4. Why prefetch routes on hover?

Question 4 options

5. What should you show while a lazy component loads?

Question 5 options

Flashcards

Question

What is native lazy loading?

Answer

A browser feature that defers loading images until they enter the viewport using loading="lazy".

Question

How do you lazy load a React component?

Answer

Use React.lazy() with dynamic import and wrap in Suspense with a fallback.

Question

What is the blur-up technique?

Answer

Showing a small, blurred placeholder image while the full image loads.

Question

Why prefetch routes?

Answer

To preload route code before navigation, making page transitions faster.

Revision Notes

Key Takeaways

  • 1. Use native loading="lazy" for images for zero-JS solution
  • 2. React.lazy with Suspense enables component-level lazy loading
  • 3. Route-based splitting loads only code for the current page
  • 4. Prefetch on hover for instant-feeling navigation
  • 5. Always show loading states for lazy loaded content

Interview Tips

  • Explain the difference between native and JavaScript lazy loading
  • Discuss when to use component vs route lazy loading
  • Know how to handle loading and error states

Cheat Sheet

Lazy Loading Cheat Sheet

Native Image Lazy Loading

<img src="image.jpg" loading="lazy" width={800} height={600} />

Component Lazy Loading

const Component = lazy(() => import('./Component'));
<Suspense fallback={<Spinner />}>
  <Component />
</Suspense>

Route Lazy Loading

const Page = lazy(() => import('./pages/Page'));

Prefetching

onMouseEnter={() => import('./Component')}