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 axios library in api.js.

Let’s update api.js:

js
// highlight-next-line
const getAxios = () => import('axios');
const MOVIE_ENDPOINT = 'https://react-intro-movies.herokuapp.com/movies';
export const loadMovies = () =>
// highlight-start
getAxios()
.then((axios) => axios.default(MOVIE_ENDPOINT))
// highlight-end
.then((res) => res.data);
export const createMovie = (movie) =>
// highlight-start
getAxios()
.then((axios) => axios.post(MOVIE_ENDPOINT, movie))
// highlight-end
.then((res) => res.data);
export const saveMovie = (movie) =>
// highlight-start
getAxios()
.then((axios) => axios.put(`${MOVIE_ENDPOINT}/${movie.id}`, movie))
// highlight-end
.then((res) => res.data);
js
// highlight-next-line
const getAxios = () => import('axios');
const MOVIE_ENDPOINT = 'https://react-intro-movies.herokuapp.com/movies';
export const loadMovies = () =>
// highlight-start
getAxios()
.then((axios) => axios.default(MOVIE_ENDPOINT))
// highlight-end
.then((res) => res.data);
export const createMovie = (movie) =>
// highlight-start
getAxios()
.then((axios) => axios.post(MOVIE_ENDPOINT, movie))
// highlight-end
.then((res) => res.data);
export const saveMovie = (movie) =>
// highlight-start
getAxios()
.then((axios) => axios.put(`${MOVIE_ENDPOINT}/${movie.id}`, movie))
// highlight-end
.then((res) => res.data);
  • The import axios from 'axios'; statement at the beginning of the file is removed.
  • We define a function getAxios, which will use dynamic import to load the code.
  • For all the code that previously use axios, not it need to call getAxios function to get the axios code, then use it to make the api calls.
  • Note that for loadMovies, which previously call axios directly with axios(MOVIE_ENDPOINT), it need to call axios.default(MOVIE_ENDPOINT) now as the result of dynamic import will cause default export to be associated to the .default properties.

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.

Do It: lazy loading ajax call code

  1. modify api.js to lazy-load axios.
  2. test the application and ensure the code still works as before.

Lazy Loading React Component

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

We would like to lazy load our Movie component.

  1. We need to export Movie component with default export
js
export const Movie = props => ();
export default Movie; // highlight-line
js
export const Movie = props => ();
export default Movie; // highlight-line
  1. Modify app.js:
jsx
import * as React from 'react';
import { BusyContainer } from './busy-container';
const Movie = React.lazy(() => import('./components/movie')); // highlight-line
function App() {
return (
<div>
<TitleBar>
<h1>React Movie App</h1>
</TitleBar>
<div className="container">
<div>
<div className="button-container">
<Button onClick={toggleShowMovies}>{moviesShown ? 'Hide' : 'Show'} Movies</Button>
</div>
{moviesShown && (
<BusyContainer isLoading={isLoading}>
{/* highlight-next-line */}
<React.Suspense fallback={<span className="spinner" />}>
{movies.map((movie) => (
<Movie
name={movie.name}
releaseDate={movie.releaseDate}
onClick={() => selectMovie(movie)}
key={movie.id}
/>
))}
{/* highlight-next-line */}
</React.Suspense>
</BusyContainer>
)}
</div>
...
</div>
</div>
);
}
export default App;
jsx
import * as React from 'react';
import { BusyContainer } from './busy-container';
const Movie = React.lazy(() => import('./components/movie')); // highlight-line
function App() {
return (
<div>
<TitleBar>
<h1>React Movie App</h1>
</TitleBar>
<div className="container">
<div>
<div className="button-container">
<Button onClick={toggleShowMovies}>{moviesShown ? 'Hide' : 'Show'} Movies</Button>
</div>
{moviesShown && (
<BusyContainer isLoading={isLoading}>
{/* highlight-next-line */}
<React.Suspense fallback={<span className="spinner" />}>
{movies.map((movie) => (
<Movie
name={movie.name}
releaseDate={movie.releaseDate}
onClick={() => selectMovie(movie)}
key={movie.id}
/>
))}
{/* highlight-next-line */}
</React.Suspense>
</BusyContainer>
)}
</div>
...
</div>
</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.

That’s it!

Do It: lazy loading React Component

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