The most surprising thing about Vite’s lazy loading is that it doesn’t actually do anything itself; it’s entirely a browser feature that Vite simply enables and leverages for efficient code splitting.

Let’s see it in action with a simple React example. Imagine you have a component that’s only needed on a specific page, like an admin dashboard. Instead of bundling it with your main application code, you can load it on demand.

// src/App.jsx
import React, { useState, lazy, Suspense } from 'react';
import './App.css';

const AdminDashboard = lazy(() => import('./AdminDashboard'));

function App() {
  const [showAdmin, setShowAdmin] = useState(false);

  return (
    <div className="App">
      <h1>My App</h1>
      <button onClick={() => setShowAdmin(true)}>Show Admin Dashboard</button>

      {showAdmin && (
        <Suspense fallback={<div>Loading Dashboard...</div>}>
          <AdminDashboard />
        </Suspense>
      )}
    </div>
  );
}

export default App;
// src/AdminDashboard.jsx
import React from 'react';

function AdminDashboard() {
  return (
    <div>
      <h2>Welcome, Administrator!</h2>
      <p>This is a privileged area.</p>
    </div>
  );
}

export default AdminDashboard;

When you build this with Vite (npm run build), and then serve the dist folder, the browser will initially download only the essential App.jsx and its dependencies. The AdminDashboard.jsx code will be in a separate chunk. Only when the user clicks the "Show Admin Dashboard" button, and the showAdmin state becomes true, will the browser encounter the lazy component and initiate a network request to fetch that specific JavaScript chunk.

Vite’s role here is primarily to configure Rollup (its underlying build tool) to correctly identify import() statements as dynamic imports. It generates the necessary manifest and chunking configuration so that when the browser requests /src/AdminDashboard.jsx (or more accurately, the hashed chunk name it resolves to), the server can deliver it. Vite’s development server also supports this, pre-emptively fetching chunks as they are dynamically imported during development, making the experience feel seamless.

The core problem this solves is application performance. By default, bundlers like Webpack or Rollup would include all your code in a single (or a few large) JavaScript file(s). This means users download everything, even features they might never use. Lazy loading, powered by dynamic import(), allows you to split your codebase into smaller, more manageable "chunks." These chunks are only downloaded by the browser when they are actually needed, significantly reducing the initial load time of your application. This is especially crucial for Single Page Applications (SPAs) where the initial JavaScript bundle can become quite large.

The exact lever you control is the import() syntax itself. Any import() call that returns a Promise resolving to a module will be treated by Vite as a candidate for code splitting. Vite’s default configuration is quite intelligent; it will automatically determine where to split based on these dynamic imports, creating separate output files (chunks) for each dynamically imported module. You don’t typically need to tweak Rollup’s chunking strategy unless you have very specific requirements for how your code is bundled.

The Suspense component in React is the companion to lazy. It allows you to gracefully handle the loading state of your lazily loaded component. While the dynamic import is being fetched, Suspense renders a fallback UI (in this case, "Loading Dashboard…"). Once the component is loaded, Suspense replaces the fallback with the actual component.

What most people miss is that import() isn’t just for components; you can dynamically import any module. This means you can lazy-load libraries, data fetching functions, utility modules, or even entire routes if you’re using a routing library that supports it. The mechanism is identical: wrap the import() call and handle the resulting promise.

The next step is often integrating this with client-side routing, where entire page components are lazily loaded based on the URL.

Want structured learning?

Take the full Vite course →