Master Code Splitting in React using dynamic imports and Suspense. Learn how to architect your app for faster initial loads and smaller bundle sizes.
Previously in this course, we discussed Establishing Performance Budgets to keep our application's footprint in check. While setting budgets prevents bloat, today we tackle the primary architectural strategy for staying within those limits: Route-level Code Splitting.
By default, bundlers like Webpack or Vite bundle your entire application into a single JavaScript file. As your app grows, this "main bundle" becomes a bottleneck, forcing users to download code for pages they haven't even visited yet. Route-level splitting allows us to break this monolith into smaller, lazily-loaded chunks.
At the heart of code splitting is the dynamic import() syntax. Unlike static import statements that are hoisted to the top of your file and included in the initial bundle, import() returns a promise that resolves to the module only when invoked.
In React, we use React.lazy to make this seamless. It lets you render a dynamic import as a regular component.
JAVASCRIPT// Instead of: import Dashboard from CE9178">'./Dashboard'; // We use: const Dashboard = React.lazy(() => import(CE9178">'./Dashboard'));
When React encounters this Dashboard component, it triggers the network request to fetch the corresponding chunk. Because this is asynchronous, we must wrap it in a Suspense boundary to provide a fallback UI while the chunk is in transit.
In our project, we are currently importing all pages in our App.js router. We need to refactor this to split our route definitions.
JSXimport { Routes, Route } from CE9178">'react-router-dom'; import Home from CE9178">'./pages/Home'; import Dashboard from CE9178">'./pages/Dashboard'; import Settings from CE9178">'./pages/Settings'; function App() { return ( <Routes> <Route path="/" element={<Home />} /> <Route path="/dashboard" element={<Dashboard />} /> <Route path="/settings" element={<Settings />} /> </Routes> ); }
To optimize, we define our lazy components and wrap the Routes component in Suspense.
JSXimport React, { Suspense, lazy } from CE9178">'react'; import { Routes, Route } from CE9178">'react-router-dom'; const Home = lazy(() => import(CE9178">'./pages/Home')); const Dashboard = lazy(() => import(CE9178">'./pages/Dashboard')); const Settings = lazy(() => import(CE9178">'./pages/Settings')); function App() { return ( <Suspense fallback={<div>Loading page...</div>}> <Routes> <Route path="/" element={<Home />} /> <Route path="/dashboard" element={<Dashboard />} /> <Route path="/settings" element={<Settings />} /> </Routes> </Suspense> ); }
This simple change instructs your bundler to create separate files for Home, Dashboard, and Settings. The browser will now only download Dashboard.js when the user navigates to /dashboard.
You cannot optimize what you cannot measure. After implementing these changes, you must verify the impact using your bundler's analysis tools.
| Tool | Purpose |
|---|---|
rollup-plugin-visualizer | Generates a treemap of your production bundle. |
webpack-bundle-analyzer | Visualizes the size of output files in Webpack. |
| Chrome DevTools (Network) | Inspects the waterfall of lazy-loaded chunks. |
When you run your build, look for the generated chunks in your dist/ or build/ folder. You should see separate files (e.g., Dashboard-xyz123.js) rather than one massive index.js.
React.lazy.Routes or the specific Route elements in Suspense with a loading spinner.Suspense too high can result in the entire layout "flashing" every time a tiny component loads. Place it as close to the lazy component as possible, or at the route level to maintain a good user experience.Suspense strategy doesn't mask these failures.By leveraging React.lazy and Suspense, we effectively decouple our application's entry point from its feature set. This reduces the initial payload, improves Time to Interactive (TTI), and aligns with our performance budgets by ensuring only necessary code is downloaded.
As we continue to build out our architecture, remember that modular directory structures make this splitting strategy significantly easier to maintain.
Up next: We will explore how to take expensive computations off the main thread using Web Workers, further ensuring our UI remains buttery smooth.
Master performance optimization in React. Learn how to use lazy loading, code splitting, and image optimization to keep your dashboard fast and efficient.
Read moreStop guessing why your app feels slow. Learn to integrate monitoring, set actionable performance alerts, and analyze real-world trends in production.
Route-level Code Splitting
Final Project Audit & Optimization
Advanced Hook Patterns
Managing Global State with Zustand/Redux
Testing Performance-Critical Components
Static Site Generation (SSG) Patterns
Internationalization (i18n) Architecture
Accessibility (a11y) in Advanced Components
Managing Third-Party Integrations
Advanced Form Handling
Using Portals for UI Overlays
Implementing Virtualized Lists
Building Design System Primitives
Managing Large-Scale Data Fetching
Micro-Frontends with React
Security Best Practices in React
Advanced Ref Usage
Memoization Pitfalls
Mastering React Patterns for Scalability
Advanced TypeScript with React