JavaScript and React course ยท Module 8: Advanced React Patterns
Code splitting
In this lesson14
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.
What is code splitting?
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.
Why do we need code splitting?
Imagine we are creating a space application with many modules:
- Mission control panel
- Astronaut monitoring system
- Galaxy map
- Discovered planets encyclopedia
- Flight simulator
- Interstellar communication center
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:
- Longer first page load time
- Higher browser memory usage
- Higher data transfer costs (especially important on mobile devices)
- Unnecessary loading and parsing of code that is not used
Benefits of code splitting
- Faster initial load - shorter time to interactivity (TTI - Time To Interactive)
- Better performance on mobile devices - lower memory and CPU usage
- Critical path prioritization - the most important parts of the application load first
- Better caching - smaller parts are easier to cache and update
- Better user experience - the application becomes responsive faster
Implementing code splitting in React
React supports code splitting through dynamic imports and the React.lazy component. Let's see how it works:
1. Dynamic import
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}2. React.lazy
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:
- We use
React.lazyto dynamically import components - We wrap components in
Suspenseto show a fallback while loading - Components are loaded only when they are needed (when the user changes the view)
Suspense
Suspense 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>Nesting 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}Route-based code splitting
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}Library code splitting
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}Preloading
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}Named exports and code splitting
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);Code splitting strategies
1. Route-based splitting
The most common and easiest approach - each page is a separate code chunk.
2. Feature-based splitting
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}3. Priority-based splitting
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}4. Vendor splitting
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};Analyzing and debugging code splitting
Analyzing bundle sizes
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}Debugging code-splitting issues
Common code splitting problems and their solutions:
Components flash during loading
- Solution: Design the fallback to match the final component layout
Too many small chunks
- Solution: Group related components in a single dynamic import
SSR (Server-Side Rendering) issues
- Solution: Use
loadable-componentsinstead ofReact.lazyfor SSR
- Solution: Use
Best practices for code splitting
Don'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)
Code splitting and TypeScript
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}Summary
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()andSuspensefor components- Dynamic
import()for modules - Splitting strategies based on routes, functionality, or priority
- Bundle analysis tools to help with decision making
By 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.
Code for this lesson: App.jsx
1import React, { createContext, useContext, useState } from 'react';
2
3// ============================================
4// PATTERN: Compound Components
5// ============================================
6// Components sharing state through Context
7
8// Context for the Accordion
9const AccordionContext = createContext();
10
11function useAccordion() {
12 const context = useContext(AccordionContext);
13 if (!context) {
14 throw new Error('Accordion components must be used within <Accordion>');
15 }
16 return context;
17}
18
19// Main Accordion component
20function Accordion({ children, defaultOpen = null }) {
21 const [openItem, setOpenItem] = useState(defaultOpen);
22
23 const toggle = (id) => {
24 setOpenItem(prev => prev === id ? null : id);
25 };
26
27 return (
28 <AccordionContext.Provider value={{ openItem, toggle }}>
29 <div style={styles.accordion}>
30 {children}
31 </div>
32 </AccordionContext.Provider>
33 );
34}
35
36// Sub-component: Item
37function AccordionItem({ children, id }) {
38 const { openItem } = useAccordion();
39 const isOpen = openItem === id;
40
41 return (
42 <div style={{
43 ...styles.item,
44 borderColor: isOpen ? '#00d4ff' : 'rgba(255,255,255,0.1)',
45 }}>
46 {React.Children.map(children, child =>
47 React.cloneElement(child, { itemId: id, isOpen })
48 )}
49 </div>
50 );
51}
52
53// Sub-component: Header
54function AccordionHeader({ children, itemId }) {
55 const { openItem, toggle } = useAccordion();
56 const isOpen = openItem === itemId;
57
58 return (
59 <button
60 onClick={() => toggle(itemId)}
61 style={styles.header}
62 >
63 <span>{children}</span>
64 <span style={{
65 transform: isOpen ? 'rotate(180deg)' : 'rotate(0)',
66 transition: 'transform 0.3s ease',
67 }}>
68 โผ
69 </span>
70 </button>
71 );
72}
73
74// Sub-component: Panel
75function AccordionPanel({ children, isOpen }) {
76 return (
77 <div style={{
78 ...styles.panel,
79 maxHeight: isOpen ? '500px' : '0',
80 padding: isOpen ? '20px' : '0 20px',
81 opacity: isOpen ? 1 : 0,
82 }}>
83 {children}
84 </div>
85 );
86}
87
88// We combine components
89Accordion.Item = AccordionItem;
90Accordion.Header = AccordionHeader;
91Accordion.Panel = AccordionPanel;
92
93// ============================================
94// PATTERN: Tabs Compound Component
95// ============================================
96
97const TabsContext = createContext();
98
99function Tabs({ children, defaultTab }) {
100 const [activeTab, setActiveTab] = useState(defaultTab);
101
102 return (
103 <TabsContext.Provider value={{ activeTab, setActiveTab }}>
104 <div style={styles.tabs}>
105 {children}
106 </div>
107 </TabsContext.Provider>
108 );
109}
110
111function TabList({ children }) {
112 return (
113 <div style={styles.tabList}>
114 {children}
115 </div>
116 );
117}
118
119function Tab({ children, id }) {
120 const { activeTab, setActiveTab } = useContext(TabsContext);
121 const isActive = activeTab === id;
122
123 return (
124 <button
125 onClick={() => setActiveTab(id)}
126 style={{
127 ...styles.tab,
128 background: isActive ? '#00d4ff' : 'transparent',
129 color: isActive ? '#000' : '#fff',
130 }}
131 >
132 {children}
133 </button>
134 );
135}
136
137function TabPanels({ children }) {
138 return <div>{children}</div>;
139}
140
141function TabPanel({ children, id }) {
142 const { activeTab } = useContext(TabsContext);
143
144 if (activeTab !== id) return null;
145
146 return (
147 <div style={styles.tabPanel}>
148 {children}
149 </div>
150 );
151}
152
153Tabs.List = TabList;
154Tabs.Tab = Tab;
155Tabs.Panels = TabPanels;
156Tabs.Panel = TabPanel;
157
158// ============================================
159// Demo App
160// ============================================
161
162function CompoundComponentsDemo() {
163 return (
164 <div style={styles.container}>
165 <header style={styles.headerSection}>
166 <h1 style={styles.title}>Compound Components</h1>
167 <p style={styles.subtitle}>
168 Design pattern for components sharing state
169 </p>
170 </header>
171
172 <div style={styles.content}>
173 {/* Accordion Demo */}
174 <div style={styles.section}>
175 <h2 style={styles.sectionTitle}>Accordion</h2>
176
177 <Accordion defaultOpen="item1">
178 <Accordion.Item id="item1">
179 <Accordion.Header>What are Compound Components?</Accordion.Header>
180 <Accordion.Panel>
181 <p>
182 Compound Components is a pattern where components share
183 internal state through Context. This allows creating
184 flexible and readable component APIs.
185 </p>
186 </Accordion.Panel>
187 </Accordion.Item>
188
189 <Accordion.Item id="item2">
190 <Accordion.Header>Pattern advantages</Accordion.Header>
191 <Accordion.Panel>
192 <ul style={{ marginLeft: '20px' }}>
193 <li>Readable and declarative syntax</li>
194 <li>Flexibility in component layout</li>
195 <li>Encapsulation of logic in the parent</li>
196 <li>Easy addition of new features</li>
197 </ul>
198 </Accordion.Panel>
199 </Accordion.Item>
200
201 <Accordion.Item id="item3">
202 <Accordion.Header>Implementation</Accordion.Header>
203 <Accordion.Panel>
204 <p>
205 Implementation relies on React Context for sharing
206 state and on static properties for grouping sub-components.
207 </p>
208 </Accordion.Panel>
209 </Accordion.Item>
210 </Accordion>
211 </div>
212
213 {/* Tabs Demo */}
214 <div style={styles.section}>
215 <h2 style={styles.sectionTitle}>Tabs</h2>
216
217 <Tabs defaultTab="tab1">
218 <Tabs.List>
219 <Tabs.Tab id="tab1">Planets</Tabs.Tab>
220 <Tabs.Tab id="tab2">Stars</Tabs.Tab>
221 <Tabs.Tab id="tab3">Galaxies</Tabs.Tab>
222 </Tabs.List>
223
224 <Tabs.Panels>
225 <Tabs.Panel id="tab1">
226 <h3>Planets of the Solar System</h3>
227 <p>Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune</p>
228 </Tabs.Panel>
229 <Tabs.Panel id="tab2">
230 <h3>Star types</h3>
231 <p>Stars are classified into dwarfs, giants, supergiants and other spectral types.</p>
232 </Tabs.Panel>
233 <Tabs.Panel id="tab3">
234 <h3>Types of galaxies</h3>
235 <p>Spiral, elliptical, irregular, and lenticular galaxies.</p>
236 </Tabs.Panel>
237 </Tabs.Panels>
238 </Tabs>
239 </div>
240 </div>
241
242 <footer style={styles.footer}>
243 <p>Compound Components โข Context API โข Static Properties</p>
244 </footer>
245 </div>
246 );
247}
248
249const styles = {
250 container: {
251 minHeight: '100vh',
252 background: 'linear-gradient(135deg, #1a1a2e 0%, #16213e 100%)',
253 padding: '30px',
254 fontFamily: "'Segoe UI', sans-serif",
255 color: '#fff',
256 },
257 headerSection: {
258 textAlign: 'center',
259 marginBottom: '40px',
260 },
261 title: {
262 fontSize: '2.5em',
263 color: '#00d4ff',
264 marginBottom: '10px',
265 },
266 subtitle: {
267 opacity: 0.7,
268 },
269 content: {
270 maxWidth: '800px',
271 margin: '0 auto',
272 },
273 section: {
274 background: 'rgba(255,255,255,0.05)',
275 borderRadius: '15px',
276 padding: '25px',
277 marginBottom: '25px',
278 border: '1px solid rgba(255,255,255,0.1)',
279 },
280 sectionTitle: {
281 color: '#ffd700',
282 marginBottom: '20px',
283 },
284 accordion: {
285 display: 'flex',
286 flexDirection: 'column',
287 gap: '10px',
288 },
289 item: {
290 borderRadius: '10px',
291 border: '2px solid',
292 overflow: 'hidden',
293 transition: 'border-color 0.3s ease',
294 },
295 header: {
296 width: '100%',
297 padding: '15px 20px',
298 background: 'rgba(255,255,255,0.05)',
299 border: 'none',
300 color: '#fff',
301 fontSize: '16px',
302 cursor: 'pointer',
303 display: 'flex',
304 justifyContent: 'space-between',
305 alignItems: 'center',
306 },
307 panel: {
308 overflow: 'hidden',
309 transition: 'all 0.3s ease',
310 lineHeight: 1.6,
311 },
312 tabs: {},
313 tabList: {
314 display: 'flex',
315 gap: '10px',
316 marginBottom: '20px',
317 },
318 tab: {
319 padding: '12px 24px',
320 border: '2px solid #00d4ff',
321 borderRadius: '8px',
322 cursor: 'pointer',
323 fontWeight: 'bold',
324 transition: 'all 0.3s ease',
325 },
326 tabPanel: {
327 padding: '20px',
328 background: 'rgba(255,255,255,0.05)',
329 borderRadius: '10px',
330 },
331 footer: {
332 textAlign: 'center',
333 marginTop: '40px',
334 opacity: 0.7,
335 },
336};
337
338export default CompoundComponentsDemo;Spotted a mistake in this lesson?
Hands-on tasks in the game
- Click in order
Arrange the syntax for React.lazy with dynamic import: