React|Optimization & Security

Bundle Size Diet: Code Splitting Strategy Using React.lazy and Suspense

1
Bundle Size Diet: Code Splitting Strategy Using React.lazy and Suspense

Chris's app is getting heavier by the day. With every new feature added, the JavaScript file size (Bundle Size) has increased honestly and steadily.

When a user first visits the site, they are forced to stare blankly at a white screen for 3 seconds. This is because the browser is busy downloading and executing a massive 5MB chunk of JavaScript.

"Wait, I only want to see the main page, so why do I have to download the code for the 'Admin Page' and the 'Chart Library' too?"

The user's complaint is valid. To resolve this inefficiency, we need to break the massive code into pieces so that users download "only what is needed right now." This is called Code Splitting.

1. The Problem with Monolithic Bundles

When you build with React (using CRA, Vite, etc.), essentially all components and libraries are merged into a single file (usually main.js or bundle.js).

  • Pros: Only one file to download, so fewer HTTP requests.
  • Cons: As the app grows, the initial load speed (TTI - Time to Interactive) slows down. Users end up downloading code for pages they will never visit.
  • We need to split this lump into smaller Chunks.

    2. Dynamic Import

    JavaScript allows you to load modules dynamically via the import() syntax.

    // Standard Import (Top of file) -> Unconditionally included in the bundle
    import { add } from './math';
    
    // Dynamic Import (Executed when needed) -> Separated into a distinct file
    const handleClick = async () => {
      const module = await import('./math');
      console.log(module.add(1, 2));
    };

    When bundlers like Webpack or Vite encounter the import() syntax, they automatically separate that file into a distinct JavaScript file during the build process.

    3. React.lazy and Suspense

    React provides React.lazy to easily apply this concept at the component level.

    Route-based Splitting

    The most effective method is to split by page (Route). If a user doesn't go to /admin, they shouldn't download the admin-related code.

    // App.tsx
    import React, { Suspense, lazy } from 'react';
    import { BrowserRouter, Routes, Route } from 'react-router-dom';
    
    // 1. Import using lazy.
    // Now, these components are separated into distinct JS files (chunks).
    const Home = lazy(() => import('./pages/Home'));
    const About = lazy(() => import('./pages/About'));
    const Admin = lazy(() => import('./pages/Admin'));
    
    function App() {
      return (
        <BrowserRouter>
          {/* 2. Configure the loading screen to show while code is fetching */}
          <Suspense fallback={<div>Loading page...</div>}>
            <Routes>
              <Route path="/" element={<Home />} />
              <Route path="/about" element={<About />} />
              <Route path="/admin" element={<Admin />} />
            </Routes>
          </Suspense>
        </BrowserRouter>
      );
    }

    Now, the moment a user clicks the /about menu, you can see a new file like src_pages_About_tsx.js being downloaded in the Network tab.

    4. Component-based Splitting

    You can split not just pages, but also specific heavy components.

    Scenario: A text editor or a complex chart library is huge in size. However, this feature is only visible when a modal window is opened.

    // EditorModal.tsx
    import React, { useState, Suspense, lazy } from 'react';
    
    // A heavy editor with a size of 1MB
    const HeavyEditor = lazy(() => import('./components/HeavyEditor'));
    
    function EditorModal() {
      const [isOpen, setIsOpen] = useState(false);
    
      return (
        <div>
          <button onClick={() => setIsOpen(true)}>Open Editor</button>
    
          {isOpen && (
            <Suspense fallback={<div>Loading Editor...</div>}>
              <HeavyEditor />
            </Suspense>
          )}
        </div>
      );
    }

    By doing this, the user downloads only the lightweight button code during the initial load, and fetches the heavy editor code only when the button is clicked.

    5. Cautions and Strategies

    Code splitting is not free. If you split too finely, the number of network requests will skyrocket, which can actually make the app slower.

    Considering UX

  • Lazy Loading Delay: If a loading spinner (Suspense) pops up every time the user navigates to a new page, it’s a bad experience.
  • Preloading: You can use a strategy where you fetch the code in advance when the user hovers over a button (Hover). This allows the page to open immediately without loading when clicked.
  • // Load in advance on mouse hover
    const onHover = () => {
      import('./pages/About'); // Send request beforehand
    };

    Key Takeaways

  • Problem: Huge bundle files slow down initial loading speeds.
  • Solution: Use React.lazy and Suspense to load code only at the moment it is needed (Lazy Loading).
  • Strategy:

  • We've now reduced the app's weight and boosted loading speed. But what if the data itself is massive? If you have to render a list of 10,000 items on the screen, the browser will freeze regardless of the bundle size.

    It's time to learn the magic of maintaining a constant number of DOM nodes: Virtualization.

    Continuing in: “Rendering Massive Data: Maintaining DOM Node Count with Virtualization (React Window)”

    🔗 References

  • React Docs - Code Splitting
  • React Docs - lazy
  • Vite - Dynamic Import
  • Comments (0)

    0/1000 characters
    Loading comments...