Modern web sites often combine all of their JavaScript into a single, large bundle. When JavaScript is served this way, loading performance suffers. Large amounts of JavaScript can also tie up the main thread, delaying interactivity. This is especially true of devices with less memory and processing power.
An alternative to large bundles is code-splitting, which is where JavaScript is split into smaller chunks. This enables sending the minimal code required to provide value up front, improving page-load times. The rest can be loaded on demand.
In this guide, we will be talking about code-splitting in the context of React, Redux, and React-Redux applications.
This guide assumes some level of familiarity with React, Redux, and React-Redux.
Code-splitting is the act of deferring the import of some portion of our JavaScript until it is needed at a later point in time by a user interaction such as button click, scrolling, typing, etc.
Code-splitting helps reduce the amount of JavaScript that is needed to make our app load as quickly as possible, thereby maximizing user engagement and improve page load times.
Code-splitting is a feature supported by bundlers like Webpack
, Rollup
and Browserify
(via factor-bundle
), which can create multiple bundles that can be dynamically loaded at runtime.
The preferred way to introduce code-splitting in a React component is via dynamic import()
. The import()
function-like form takes the module name as an argument and returns a Promise
, which always resolves to the namespace object of the module.
1import('./Module').then(Module => Module.method())
1// SomeComponent.js
2const SomeComponent = () => <p>This is a test component</p>
3
4export default SomeComponent
1// App.js
2import React, { Component } from 'react'
3
4class App extends Component {
5 handleClick = () => {
6 import('./SomeComponent')
7 .then(({ SomeComponent }) => {
8 // Use SomeComponent
9 })
10 .catch(err => {
11 // Handle failure
12 });
13 };
14
15 render() {
16 return (
17 <div>
18 <button onClick={this.handleClick}>Click Me</button>
19 </div>
20 );
21 }
22}
23
24export default App;
The example above includes SomeComponent.js
as a separate chunk that only loads after the Click Me button has been clicked.
React.lazy
method makes it easy to code-split a React application on a component level using dynamic imports.
React.lazy
takes a function that must call a dynamic import()
. This must return a Promise
which resolves to a module with a default
export containing a React component.
1const SomeComponent = React.lazy(() => import('./SomeComponent'));
The lazy component should then be rendered inside a Suspense
component, which allows us to show some fallback content (such as a loading indicator) while we’re waiting for the lazy component to load.
1const SomeComponent = React.lazy(() => import('./SomeComponent'));
2
3function MyComponent() {
4 return (
5 <div>
6 <Suspense fallback={<div>Loading...</div>}>
7 <SomeComponent />
8 </Suspense>
9 </div>
10 );
11}
Suspense
fallback props accepts any React element which is being rendered while waiting for the component to load. We can place the Suspense
component anywhere above the lazy component. We can even wrap multiple lazy components with a single Suspense
component.
1const SomeComponent = React.lazy(() => import('./SomeComponent'));
2const AnotherComponent = React.lazy(() => import('./AnotherComponent'));
3
4function MyComponent() {
5 return (
6 <div>
7 <Suspense fallback={<div>Loading...</div>}>
8 <SomeComponent />
9 <AnotherComponent />
10 </Suspense>
11 </div>
12 );
13}
React.lazy
currently only supports default exports.
Here’s an example of how to setup route-based code-splitting into our app using libraries like React Router with React.lazy
.
1// App.js
2import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
3import React, { Suspense, lazy } from 'react';
4
5const Home = lazy(() => import('./routes/Home'));
6const About = lazy(() => import('./routes/About'));
7
8const App = () => (
9 <Router>
10 <Suspense fallback={<div>Loading...</div>}>
11 <Switch>
12 <Route exact path="/" component={Home} />
13 <Route path="/about" component={About} />
14 </Switch>
15 </Suspense>
16 </Router>
17);
From the previous sections, we have been able to demonstrate how we can load our React components dynamically, but we still need to get the right data into our modules as they load.
Redux as a state management library allows us to provide reducer functions at the time we create the store with createStore
function but does not give us the ability to to register reducer functions on demand. So how do we achieve that?
It turns out Redux store API exposes a replaceReducer
function that replaces the current active root reducer function with a new root reducer function.
1// store.js
2import { combineReducers, createStore } from "redux"
3
4const initialState = {}
5const store = createStore(createReducer(), initialState)
6
7const newRootReducer = combineReducers({
8 existingSlice: existingSliceReducer,
9 newSlice: newSliceReducer
10})
11
12store.replaceReducer(newRootReducer)
We could go one step further by creating a reusable injectReducer
function in addition to the replaceReducer
that keeps references to all of the existing slice reducers, and attach that to the store instance.
1// reducers.js
2import { combineReducers } from 'redux'
3
4const createReducer = (asyncReducers) => {
5 return combineReducers({
6 ...asyncReducers
7 })
8}
9
10export default createReducer
1// store.js
2import { createStore } from "redux"
3import createReducer from "./reducers"
4
5const store = createStore(createReducer())
6
7export default function configureStore() {
8 // Add a dictionary to keep track of the registered async reducers
9 store.asyncReducers = {}
10
11 // Create an inject reducer function
12 // This function adds the async reducer, and creates a new combined reducer
13 store.injectReducer = (key, asyncReducer) => {
14 store.asyncReducers[key] = asyncReducer
15 store.replaceReducer(createReducer(store.asyncReducers))
16 }
17
18 // Return the modified store
19 return store
20}
21
22export function getStore() {
23 return store
24}
Note: injectReducer
is not part of the redux store API.
The usage looks like this:
1// App.js
2import React from "react";
3import { getStore } from '../store'
4
5const store = getStore()
6
7const Section = React.lazy(() => import('../containers/Section')
8 .then(async module => {
9 const todos = await import('../reducers/todos')
10 .then(todosModule => todosModule.default)
11
12 store.injectReducer('todos', todos)
13 return module
14}))
15
16const App = () => (
17 <React.Suspense fallback={<div>loading...</div>}>
18 <MainSection />
19 </React.Suspense>
20);
21
22export default App;
A few Redux libraries that are worth checking out:
The link below points to a working example of a todo application that uses React.lazy
, React.Suspense
and Redux
.
In this guide, we have learned how code-splitting can help us improve loading time and page performance by loading Javascript on demand until it is needed.
Here are a few resources on Code-Splitting to check out: