Code Splitting

In most large-scale React applications, it is very common that your users would not need all features everytime they access your applications. Therefore, it’s better if you can split the code to multiple chunks and only send down the chunks that is required (lazy-loading).

This used to be very difficult to do in a scalable way. However, thanks to webpack (and other bundler like Parcel), it has becoming very easy to setup.

We will starts with lazy-loading some Javascript code, then lazy-loading React component.

Lazy Loading JS code

To start lazy-loading JS code, the most straightforward way is through the dynamic import() syntax.

We will lazy load the code in api.js.

src/app.js
jsx
import * as React from 'react';
import { BusyContainer } from './busy-container';
import Movie from './movie';
// highlight-start
const loadCodeAndMovies = () =>
import('./api').then(({ loadMovies }) => loadMovies());
// highlight-end
class App extends React.Component {
state = {
showMovies: false,
movies: [],
isLoading: true
};
componentDidMount() {
// highlight-next-line
loadCodeAndMovies().then(movies =>
this.setState({ movies, isLoading: false })
);
}
...
}
export default App;
src/app.js
jsx
import * as React from 'react';
import { BusyContainer } from './busy-container';
import Movie from './movie';
// highlight-start
const loadCodeAndMovies = () =>
import('./api').then(({ loadMovies }) => loadMovies());
// highlight-end
class App extends React.Component {
state = {
showMovies: false,
movies: [],
isLoading: true
};
componentDidMount() {
// highlight-next-line
loadCodeAndMovies().then(movies =>
this.setState({ movies, isLoading: false })
);
}
...
}
export default App;
  • The import { loadMovies } from './api'; statement at the beginning of the file is removed.
  • We define a function loadCodeAndMoviesData, which will use dynamic import to load the code and then use the loaded function loadMovies to make the ajax call.
  • In the componentDidMount, we use loadCodeAndMoviesData to get the movies from backend.

When you try to compile the code by npm start now, you would get a syntax error. This is because similar to class properties, dynamic import is not included as part of preset-env nor preset-react, so we need to install additional plugin

  1. install a babel plugin as devDependency:

    bash
    npm install -D @babel/plugin-syntax-dynamic-import
    bash
    npm install -D @babel/plugin-syntax-dynamic-import
  2. update .babelrc:

    .babelrc
    json
    {
    "presets": ["@babel/preset-env", "@babel/preset-react"],
    "plugins": ["@babel/plugin-proposal-class-properties", "@babel/plugin-syntax-dynamic-import"]
    }
    .babelrc
    json
    {
    "presets": ["@babel/preset-env", "@babel/preset-react"],
    "plugins": ["@babel/plugin-proposal-class-properties", "@babel/plugin-syntax-dynamic-import"]
    }
  3. run npm start again.

Now you would see the following output:

bash
Hash: f9f3a764f70be0b6cc25
Version: webpack 4.28.4
Time: 1229ms
Built at: 2019-01-07 21:45:12
Asset Size Chunks Chunk Names
0.js 57.2 KiB 0 [emitted]
1.js 1000 bytes 1 [emitted]
index.html 470 bytes [emitted]
main.js 1.18 MiB main [emitted] main
Entrypoint main = main.js
bash
Hash: f9f3a764f70be0b6cc25
Version: webpack 4.28.4
Time: 1229ms
Built at: 2019-01-07 21:45:12
Asset Size Chunks Chunk Names
0.js 57.2 KiB 0 [emitted]
1.js 1000 bytes 1 [emitted]
index.html 470 bytes [emitted]
main.js 1.18 MiB main [emitted] main
Entrypoint main = main.js

And from the Network tab of your DevTools, you should be able to see chunk 0.js and 1.js are loaded.

Exercise

  1. modify app.js to lazy-load api.js.
  2. configure Babel as described.
  3. test the application and ensure the code still works as before.

Commit: 140-lazyload-code

Lazy Loading React Component

Once you understand dynamic import() for JS code, lazy-loading React Components is just using it with some React helper.

Let’s lazy load our Movie components by modify app.js:

src/app.js
jsx
import * as React from 'react';
import { BusyContainer } from './busy-container';
const Movie = React.lazy(() =>
import(/* webpackChunkName: "Movie" */ './movie')
);
const loadCodeAndMovies = () =>
import(/* webpackChunkName: "api" */ './api').then(({ loadMovies }) =>
loadMovies()
);
class App extends React.Component {
...
render() {
return (
<div>
<div className="title-bar">
<h1>React Movie App</h1>
</div>
<div className="button-container">
<button onClick={this.toggleMovies} className="button">
{this.state.showMovies ? 'Hide' : 'Show'} Movies
</button>
</div>
{this.state.showMovies && (
<React.Suspense fallback={<span>Loading Component...</span>}>
<BusyContainer isLoading={this.state.isLoading}>
{this.state.movies.map(movie => (
<Movie
name={movie.name}
releaseDate={movie.releaseDate}
key={movie.id}
/>
))}
</BusyContainer>
</React.Suspense>
)}
</div>
);
}
}
export default App;
src/app.js
jsx
import * as React from 'react';
import { BusyContainer } from './busy-container';
const Movie = React.lazy(() =>
import(/* webpackChunkName: "Movie" */ './movie')
);
const loadCodeAndMovies = () =>
import(/* webpackChunkName: "api" */ './api').then(({ loadMovies }) =>
loadMovies()
);
class App extends React.Component {
...
render() {
return (
<div>
<div className="title-bar">
<h1>React Movie App</h1>
</div>
<div className="button-container">
<button onClick={this.toggleMovies} className="button">
{this.state.showMovies ? 'Hide' : 'Show'} Movies
</button>
</div>
{this.state.showMovies && (
<React.Suspense fallback={<span>Loading Component...</span>}>
<BusyContainer isLoading={this.state.isLoading}>
{this.state.movies.map(movie => (
<Movie
name={movie.name}
releaseDate={movie.releaseDate}
key={movie.id}
/>
))}
</BusyContainer>
</React.Suspense>
)}
</div>
);
}
}
export default App;
  • We wrap dynamic import statement with React.lazy, so that React knows this is a lazy-loaded Component.
  • We wrap lazy-loaded component with React.Suspense so that React will fallback to the loading indicator whenever any component within the React.Suspense is waiting to be loaded.
  • The comment /* webpackChunkName: "Movie" */ is known as webpack magic comment. It allows us to name our chunk with a meaningful name like api.js instead of 0.js. You can read about it in this section of webpack docs.

That’s it!

Exercise

  1. modify app.js to lazy-load Movie component.
  2. test the application and ensure the code still works as before.

Commit: 150-lazyload-component

(Optional) Fix Failing Jest Tests due to Dynamic Import

Because Jest runs in NodeJS, which doesn’t understand dynamic import syntax, you would encounter syntax error if you try to run the tests now. Some tweaks is required in Babel.

See the following branch for the required changes.

Commit: 151-lazyload-jest-fix