We use cookies to enhance your experience on the site
CodeWorlds

Code splitting

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.

Go to CodeWorlds