🚀 Code Splitting in React: Make Your App Load Faster Without Sacrificing Features
Imagine this: You build a stunning dashboard app packed with advanced visualizations using libraries like D3 and Three.js. Your users are excited—until they open your app and the login screen takes forever to load. What happened?
The answer: You’re loading too much JavaScript up front.
In this guide, you’ll learn how to fix that using code splitting—an essential technique for improving load times in modern React apps. You’ll explore:
React.lazy()
and <Suspense />
import()
useSpinDelay
to improve UXLet’s dive in. 🏊♂️
Code splitting is the practice of breaking your JavaScript bundle into smaller chunks that load only when needed. Instead of shipping all features to the user upfront, you load parts of the code on demand—especially expensive features that aren’t used immediately.
Think of it like Netflix only streaming the episode you’re watching—not the entire season at once.
Here’s a simple example of dynamic imports using native JavaScript:
import('/some-module.js').then((module) => {// Use module exports here},(error) => {// Handle loading error});
React takes this a step further with built-in support for lazy-loading components.
React’s lazy()
function allows you to dynamically import components. You wrap the lazy-loaded component with <Suspense />
to show a fallback UI while the code loads.
// SmileyFace.tsxexport default function SmileyFace() {return <div>😃</div>;}// App.tsximport { lazy, Suspense } from 'react';const SmileyFace = lazy(() => import('./SmileyFace'));function App() {return (<div><Suspense fallback={<div>Loading...</div>}><SmileyFace /></Suspense></div>);}
This allows the bundle containing SmileyFace
to be excluded from the main bundle—only fetched when needed.
🛠️ Try using Chrome’s Coverage tool to identify which code is unused at initial load.
Let’s say your app has a beautiful 3D globe using heavy libraries like three.js
. It’s impressive—but it shouldn’t slow down the login screen.
Only load the globe code after the user clicks “Show Globe.”
import { lazy, Suspense, useState } from 'react';const Globe = lazy(() => import('./Globe'));function App() {const [showGlobe, setShowGlobe] = useState(false);return (<div><label><inputtype="checkbox"onChange={(e) => setShowGlobe(e.target.checked)}/>Show Globe</label><Suspense fallback={<div>Loading globe...</div>}>{showGlobe && <Globe />}</Suspense></div>);}
Open the Network tab in DevTools and watch the JavaScript chunk for Globe
load on demand when you click the checkbox.
🧪 Bonus: Throttle the network to “Slow 3G” to simulate real-world loading scenarios.
Sometimes, lazy loading introduces delays that hurt UX. What if 99% of users end up clicking “Show Globe” anyway? Waiting twice—first for the app to load, then for the globe—is annoying.
Start loading the module in the background as soon as the user hovers or focuses on the button.
let preloadGlobe = () => import('./Globe');function App() {const [showGlobe, setShowGlobe] = useState(false);const Globe = lazy(() => preloadGlobe());return (<div><labelonPointerOver={preloadGlobe}onFocus={preloadGlobe}><inputtype="checkbox"onChange={(e) => setShowGlobe(e.target.checked)}/>Show Globe</label><Suspense fallback={<div>Loading globe...</div>}>{showGlobe && <Globe />}</Suspense></div>);}
Now, the module begins loading before the user even clicks, leading to a faster, smoother experience.
📦 Pro Tip: Calling import()
multiple times is fine—your bundler will ensure it only loads once.
React shows the <Suspense fallback />
immediately—even if the lazy component is already loaded. This can lead to an awkward flash of “Loading…” even when the component is ready.
To fix this, you can use useTransition
to tell React this update is non-urgent. This delays the fallback UI and avoids flashing.
import { lazy, Suspense, useTransition } from 'react';const Globe = lazy(() => import('./Globe'));function App() {const [showGlobe, setShowGlobe] = useState(false);const [isPending, startTransition] = useTransition();return (<div><labelonPointerOver={() => import('./Globe')}onFocus={() => import('./Globe')}><inputtype="checkbox"onChange={(e) => {startTransition(() => {setShowGlobe(e.target.checked);});}}/>Show Globe</label>{isPending && <span>Preparing globe...</span>}<Suspense fallback={<div>Loading globe...</div>}>{showGlobe && <Globe />}</Suspense></div>);}
You can also use the spin-delay
package to delay the fallback from appearing too quickly:
import { useSpinDelay } from 'spin-delay';const showFallback = useSpinDelay(isPending, {delay: 300,minDuration: 500});
This adds polish and prevents jarring flashes of UI.
Technique | Purpose | When to Use |
---|---|---|
React.lazy() + <Suspense /> | Load components on demand | For large, infrequently used features |
import() on hover / focus | Preload eagerly | For features users often trigger |
useTransition | Defer low-priority UI changes | For lazy components to avoid flashing |
useSpinDelay | Smooth loading transitions | When network speed is inconsistent |
Use Chrome DevTools:
Quick Links
Legal Stuff
Social Media