JavaScript and React course ยท Module 8: Advanced React Patterns

Code splitting

10 min read
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:

  1. We use React.lazy to dynamically import components
  2. We wrap components in Suspense to show a fallback while loading
  3. 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:

  1. Components flash during loading

    • Solution: Design the fallback to match the final component layout
  2. Too many small chunks

    • Solution: Group related components in a single dynamic import
  3. SSR (Server-Side Rendering) issues

    • Solution: Use loadable-components instead of React.lazy for SSR

Best practices for code splitting

  1. Don't overdo the splitting - too many small chunks can lead to performance issues due to numerous HTTP requests

  2. Analyze your bundles - regularly monitor the size and contents of bundles

  3. 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));
  4. Use code splitting together with caching - properly configured cache headers for chunks can significantly improve performance

  5. 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() and Suspense for 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:

Useful articles