As our cosmic React application grows, the JavaScript file that loads in the browser also gets bigger. This can lead to longer page load times, higher memory usage, and generally worse performance. Code splitting is a technique that allows you to "split" the application into smaller parts that can be loaded dynamically, only when they are needed.
Code splitting is a technique that divides a large JavaScript bundle into smaller chunks that can be loaded on demand or in parallel. Instead of sending the entire application to the user at once, we send only what is needed for the initial render, and the rest is loaded in the background or when needed.
It is like launching a space mission - we don't send all the resources at once, but in stages: first the main ship, then additional modules, and finally smaller tools and equipment when they become needed.
Imagine we are creating a space application with many modules:
If we build the entire application as a single bundle, the user must download all that code even if they just want to check the galaxy map. As a result:
React supports code splitting through dynamic imports and the
React.lazy component. Let's see how it works:The
import() function is a standard ES function that allows dynamically importing modules. Instead of loading all dependencies at the top of the file, we can load them only when they are needed:1// Without code splitting
2import { calculateOrbit } from './complexOrbitCalculations';
3
4function OrbitCalculator({ planetData }) {
5 const orbitInfo = calculateOrbit(planetData);
6 return <div>{/* Rendering results */}</div>;
7}
8
9// With code splitting
10function OrbitCalculator({ planetData }) {
11 const [orbitInfo, setOrbitInfo] = useState(null);
12 const [loading, setLoading] = useState(true);
13
14 useEffect(() => {
15 // Dynamic module import only when the component mounts
16 import('./complexOrbitCalculations')
17 .then(module => {
18 const result = module.calculateOrbit(planetData);
19 setOrbitInfo(result);
20 setLoading(false);
21 });
22 }, [planetData]);
23
24 if (loading) return <p>Calculating orbit...</p>;
25 return <div>{/* Rendering results */}</div>;
26}React.lazy is a function that allows rendering a dynamic import as a regular component. It only works with default exports.1import React, { Suspense } from 'react';
2
3// Regular import
4// import GalaxyMap from './GalaxyMap';
5
6// Lazy import
7const GalaxyMap = React.lazy(() => import('./GalaxyMap'));
8const PlanetDetails = React.lazy(() => import('./PlanetDetails'));
9const SpaceshipControls = React.lazy(() => import('./SpaceshipControls'));
10
11function SpaceExplorer() {
12 const [view, setView] = useState('map');
13
14 return (
15 <div className="space-explorer">
16 <nav>
17 <button onClick={() => setView('map')}>Galaxy Map</button>
18 <button onClick={() => setView('planets')}>Planet Details</button>
19 <button onClick={() => setView('controls')}>Control Panel</button>
20 </nav>
21
22 <Suspense fallback={<div>Loading module...</div>}>
23 {view === 'map' && <GalaxyMap />}
24 {view === 'planets' && <PlanetDetails />}
25 {view === 'controls' && <SpaceshipControls />}
26 </Suspense>
27 </div>
28 );
29}In the above example:
React.lazy to dynamically import componentsSuspense to show a fallback while loadingSuspense is a React component that allows "suspending" rendering until certain conditions are met (e.g., data is loaded or component code is available).Suspense accepts a
fallback prop, which is rendered while waiting:1<Suspense fallback={<LoadingSpinner message="Preparing module..." />}>
2 <LazilyLoadedComponent />
3</Suspense>Suspense can handle multiple Lazy components at once:
1<Suspense fallback={<LoadingSpinner />}>
2 <LazyComponent1 />
3 <div>
4 <LazyComponent2 />
5 </div>
6 <LazyComponent3 />
7</Suspense>We can also nest Suspense components to achieve more granular control over what is displayed during loading:
1function SpaceStation() {
2 return (
3 <div>
4 <h1>Space Station Alpha</h1>
5
6 {/* Main layout loads immediately */}
7 <StationLayout />
8
9 {/* Map loads with its own fallback */}
10 <Suspense fallback={<p>Loading station map...</p>}>
11 <StationMap />
12 </Suspense>
13
14 {/* Control panel has nested lazy components */}
15 <Suspense fallback={<p>Preparing control panel...</p>}>
16 <ControlPanel>
17 <Suspense fallback={<p>Loading life support systems...</p>}>
18 <LifeSupportMonitor />
19 </Suspense>
20
21 <Suspense fallback={<p>Loading engine controls...</p>}>
22 <EngineControls />
23 </Suspense>
24 </ControlPanel>
25 </Suspense>
26 </div>
27 );
28}One of the most common ways to apply code splitting is dividing code based on routes. Each page or view becomes a separate code chunk:
1import React, { Suspense, lazy } from 'react';
2import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
3import Navbar from './Navbar';
4import LoadingPage from './LoadingPage';
5
6// Lazy loading pages
7const MissionControl = lazy(() => import('./pages/MissionControl'));
8const GalaxyMap = lazy(() => import('./pages/GalaxyMap'));
9const CrewManagement = lazy(() => import('./pages/CrewManagement'));
10const ResearchDatabase = lazy(() => import('./pages/ResearchDatabase'));
11const SpaceshipConfig = lazy(() => import('./pages/SpaceshipConfig'));
12
13function App() {
14 return (
15 <Router>
16 <Navbar />
17 <Suspense fallback={<LoadingPage />}>
18 <Switch>
19 <Route exact path="/" component={MissionControl} />
20 <Route path="/galaxy-map" component={GalaxyMap} />
21 <Route path="/crew" component={CrewManagement} />
22 <Route path="/research" component={ResearchDatabase} />
23 <Route path="/spaceship" component={SpaceshipConfig} />
24 </Switch>
25 </Suspense>
26 </Router>
27 );
28}We can also apply code splitting for large libraries that are not needed immediately:
1import React, { useState } from 'react';
2
3function DataVisualizer({ data }) {
4 const [charts, setCharts] = useState(null);
5 const [showCharts, setShowCharts] = useState(false);
6
7 const loadChartsLibrary = async () => {
8 // Load the library only when the user wants to see charts
9 const D3Module = await import('d3');
10 const ChartModule = await import('./ChartComponent');
11
12 setCharts({
13 d3: D3Module.default,
14 ChartComponent: ChartModule.default
15 });
16 };
17
18 const handleShowCharts = () => {
19 if (!charts) {
20 setShowCharts(true);
21 loadChartsLibrary();
22 } else {
23 setShowCharts(true);
24 }
25 };
26
27 return (
28 <div className="data-visualizer">
29 <h2>Space Exploration Data</h2>
30 <button onClick={handleShowCharts}>
31 {showCharts ? 'Hide charts' : 'Show charts'}
32 </button>
33
34 {showCharts && (
35 <>
36 {charts ? (
37 <charts.ChartComponent data={data} d3={charts.d3} />
38 ) : (
39 <p>Loading advanced charts...</p>
40 )}
41 </>
42 )}
43 </div>
44 );
45}Sometimes we know that a component will be needed soon (e.g., when the user hovers over a button that will show a modal). We can start loading the module earlier:
1const PlanetExplorer = React.lazy(() => import('./PlanetExplorer'));
2
3function PlanetInfoButton({ planetId }) {
4 const [showExplorer, setShowExplorer] = useState(false);
5
6 const handleMouseEnter = () => {
7 // Start downloading the module when the user hovers over the button
8 const PlanetExplorerModule = import('./PlanetExplorer');
9 };
10
11 return (
12 <>
13 <button
14 onMouseEnter={handleMouseEnter}
15 onClick={() => setShowExplorer(true)}
16 >
17 Explore planet {planetId}
18 </button>
19
20 {showExplorer && (
21 <Suspense fallback={<p>Preparing explorer...</p>}>
22 <PlanetExplorer planetId={planetId} />
23 </Suspense>
24 )}
25 </>
26 );
27}React.lazy only works with default exports. If we want to use named exports, we need to create an intermediate module:1// MathUtils.js
2export const calculateDistance = (a, b) => { /* ... */ };
3export const calculateVelocity = (distance, time) => { /* ... */ };
4export const calculateAcceleration = (velocity, time) => { /* ... */ };
5
6// DistanceCalculator.js (intermediate module)
7export { calculateDistance } from './MathUtils';
8export default calculateDistance;
9
10// Usage
11const DistanceCalculator = React.lazy(() => import('./DistanceCalculator'));Alternatively, we can manually handle the import and extract the desired export:
1const calculateVelocity = React.lazy(() =>
2 import('./MathUtils').then(module => ({ default: module.calculateVelocity }))
3);The most common and easiest approach - each page is a separate code chunk.
Components related to a specific functionality (e.g., admin panel, advanced analytics) are grouped together.
1// Loading the entire admin module on demand
2const AdminDashboard = React.lazy(() => import('./admin/Dashboard'));
3
4function App() {
5 const [isAdmin, setIsAdmin] = useState(false);
6
7 // isAdmin is set after user login
8
9 return (
10 <div>
11 {isAdmin ? (
12 <Suspense fallback={<p>Loading admin panel...</p>}>
13 <AdminDashboard />
14 </Suspense>
15 ) : (
16 <UserDashboard />
17 )}
18 </div>
19 );
20}Critical components loaded immediately, less important ones lazily.
1// Critical components imported normally
2import CriticalSystemStatus from './CriticalSystemStatus';
3import NavigationControls from './NavigationControls';
4
5// Less important components loaded lazily
6const SystemLogs = React.lazy(() => import('./SystemLogs'));
7const CrewSchedule = React.lazy(() => import('./CrewSchedule'));
8const MaintenanceStatus = React.lazy(() => import('./MaintenanceStatus'));
9
10function SpaceshipDashboard() {
11 return (
12 <div className="dashboard">
13 <div className="critical-section">
14 <CriticalSystemStatus />
15 <NavigationControls />
16 </div>
17
18 <Suspense fallback={<p>Loading additional modules...</p>}>
19 <div className="secondary-section">
20 <SystemLogs />
21 <CrewSchedule />
22 <MaintenanceStatus />
23 </div>
24 </Suspense>
25 </div>
26 );
27}Splitting large external libraries into separate chunks.
In webpack, this can be achieved using the
splitChunks configuration:1module.exports = {
2 //...
3 optimization: {
4 splitChunks: {
5 chunks: 'all',
6 maxInitialRequests: Infinity,
7 minSize: 0,
8 cacheGroups: {
9 vendor: {
10 test: /[\/]node_modules[\/]/,
11 name(module) {
12 // get the name. E.g. node_modules/packageName/sub/path
13 // or node_modules/packageName
14 const packageName = module.context.match(
15 /[\/]node_modules[\/](.*?)([\/]|$)/
16 )[1];
17
18 // npm package names are URL-safe, but some servers don't like @ symbols
19 return `npm.${packageName.replace('@', '')}`;
20 },
21 },
22 },
23 },
24 },
25};Tools like "webpack-bundle-analyzer" or "source-map-explorer" help visualize bundle contents and identify splitting opportunities:
1# Installation
2npm install --save-dev webpack-bundle-analyzer
3
4# Configuration in webpack.config.js
5const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
6
7module.exports = {
8 plugins: [
9 new BundleAnalyzerPlugin()
10 ]
11}Common code splitting problems and their solutions:
Components flash during loading
Too many small chunks
SSR (Server-Side Rendering) issues
loadable-components instead of React.lazy for SSRDon't overdo the splitting - too many small chunks can lead to performance issues due to numerous HTTP requests
Analyze your bundles - regularly monitor the size and contents of bundles
Use Magic Comments in webpack for better control over chunks:
1// Named chunk
2const HomeComponent = React.lazy(() => import(
3 /* webpackChunkName: "home" */
4 './HomeComponent'
5));
6
7// Prefetch - suggest to the browser that this resource will be needed
8const SettingsComponent = React.lazy(() => import(
9 /* webpackChunkName: "settings" */
10 /* webpackPrefetch: true */
11 './SettingsComponent'
12));
13
14// Preload - load this resource with high priority
15const ProfileComponent = React.lazy(() => import(
16 /* webpackChunkName: "profile" */
17 /* webpackPreload: true */
18 './ProfileComponent'
19));Use code splitting together with caching - properly configured cache headers for chunks can significantly improve performance
Monitor the impact on performance - use tools like Lighthouse to measure load times and TTI (Time To Interactive)
TypeScript works well with code splitting but requires a few additional steps:
1// Make sure tsconfig.json has:
2// "module": "esnext"
3
4// Dynamic import with typing
5const DataVisualization = React.lazy(() =>
6 import('./DataVisualization') as Promise<{ default: React.ComponentType<DataVisProps> }>
7);
8
9interface DataVisProps {
10 data: Array<DataPoint>;
11 width: number;
12 height: number;
13}
14
15interface DataPoint {
16 x: number;
17 y: number;
18 label: string;
19}
20
21function Dashboard() {
22 const [data, setData] = useState<Array<DataPoint>>([]);
23
24 return (
25 <div>
26 <h1>Space Mission Panel</h1>
27
28 <Suspense fallback={<div>Loading visualization...</div>}>
29 <DataVisualization
30 data={data}
31 width={800}
32 height={400}
33 />
34 </Suspense>
35 </div>
36 );
37}Code splitting is a powerful optimization technique that allows for significantly improving the performance of React applications, especially for larger projects. Thanks to code splitting, users load only what they need, which leads to faster application startup and a better user experience.
Key tools and methods include:
React.lazy() and Suspense for componentsimport() for modulesBy implementing code splitting in our cosmic application, we can ensure that users quickly gain access to the most important features, and additional resources are loaded when they are needed - similar to a multi-stage space mission where each module is delivered at the right time.