title: Implementing Code Splitting with Webpack 4 and React Router 4
date: 2018-03-30 14:05:16
banner: https://cdn.statically.io/gh/YanYuanFE/picx-images-hosting@master/20231128/photo-1416339442236-8ceb164046f8.2vwnww6auku0.webp
tags:
- React
- react-router
- Webpack
Webpack's Code Splitting feature allows you to separate code into different bundles, which can then be loaded on demand or in parallel.
What is Code Splitting#
When using Webpack to bundle a React application, Webpack bundles the entire application into a single JS file. When a user visits the initial page, the entire JS file is loaded at once, which can slow down the initial rendering speed. To address this issue, Webpack introduced the Code Splitting feature, which allows you to separate code into different bundles and load them on demand or in parallel. Code splitting can be used to create smaller bundles and control resource loading priorities, which can greatly impact loading times. There are three commonly used methods for code splitting:
- Entry Points: Manually separate code using the entry configuration.
- Prevent Duplication: Use the CommonsChunkPlugin to deduplicate and separate chunks.
- Dynamic Imports: Separate code using inline function calls in modules.
This article will only discuss the dynamic imports method.
Dynamic Imports#
When it comes to dynamic code splitting, Webpack provides two similar techniques. The first and preferred method is to use the import() syntax, which is part of the ECMAScript proposal. The second method is to use the webpack-specific require.ensure. Let's first try using the first method with react-router 4...
Note: The import() call uses promises internally. If you're using import() in older browsers, make sure to use a polyfill library (such as es6-promise or promise-polyfill) to shim Promise.
Now let's see how to implement code splitting with react-router 4.
React Code Splitting and Lazy Load#
On the path to implementing code splitting with react-router 4, the community has already developed mature third-party libraries, such as react-loadable. Here, we will explain how to achieve code splitting without relying on third-party libraries.
Assuming you already have a basic understanding of react, react-router 4, and webpack, and have set up a simple development environment, let's take a look at the entry file for our project.
index.js under src:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('app'));
In App.js under src:
import React from 'react';
import ReactDOM from 'react-dom';
import { Route, Switch, BrowserRouter } from 'react-router-dom';
import Routes from './routes';
export default () => (
<BrowserRouter>
<Switch>
{
Routes.map(({name, path, exact = true, component }) => (
<Route path={path} exact={exact} component={component} key={name} />
))
}
</Switch>
</BrowserRouter>
)
In App.js, we use react-router-dom for routing, and routes.js as the routing configuration file. Below, we use the routing configuration to generate route components.
Now let's take a look at the routing configuration in routes.js.
import AsyncComponent from './components/acync-component';
export default [
{
name: 'Home',
icon: 'home',
path: '/',
component: AsyncComponent(() => import('./containers/home'))
},
{
name: 'Detail',
icon: 'detail',
path: '/detail',
component: AsyncComponent(() => import('./containers/detail'))
}
]
In routes.js, we configure the parameters needed for the route components. It's important to note that we use an asynchronous component, AsyncComponent, in the route parameters. Here, we don't import the component directly, but instead pass a function to AsyncComponent, which will dynamically import the component when the AsyncComponent(() => import('./containers/home')) component is created. This approach of passing a function as a parameter instead of a string allows webpack to recognize that code splitting is needed.
Using import() requires using Babel preprocessors and the Syntax Dynamic Import Babel Plugin. Since import() returns a promise, it can be used with async functions. To use async functions, you need to use the babel-plugin-transform-runtime.
Install the Babel plugins:
npm i -D babel-plugin-syntax-dynamic-import babel-plugin-transform-runtime
Other Babel plugins used in this project include babel-core, babel-loader, babel-preset-env, and babel-preset-react, which are mainly used for JSX compilation.
Configure .babelrc
Create a .babelrc file in the root directory and configure it as follows:
{
"presets":["react","env"],
"plugins": ["syntax-dynamic-import", "transform-runtime"]
}
AsyncComponent Higher-Order Component#
The AsyncComponent for this project is located in src/components/async-component/index.js, and the code is as follows:
import React, { Component } from 'react';
export default (loadComponent, placeholder = 'Loading...') => {
return class AsyncComponent extends Component {
constructor() {
super();
this.state = {
Child: null
}
this.unmount = false;
}
componentWillUnmount() {
this.unmount = true;
}
async componentDidMount() {
const { default: Child } = await loadComponent();
if (this.unmount) return;
this.setState({
Child
})
}
render() {
const { Child } = this.state;
return (
Child ? <Child {...this.props} /> : placeholder
)
}
}
}
First, this is a higher-order component that returns a new component. It takes two parameters: the method for dynamically loading the component and a placeholder for when the component is loading (which can also be a Loading component).
Inside the returned AsyncComponent, in the constructor, we initialize the state with Child set to null and define this.unmount as false to indicate whether the component has been unmounted.
We use async to define an asynchronous method in componentDidMount, where we use await to asynchronously execute the first parameter passed to the function, which dynamically loads the component for the current route.
Note:
When calling the import() method of ES6 modules (importing modules), you must refer to the .default value of the module, as it is the actual module object returned after the promise is resolved.
Therefore, we use object destructuring in ES6 to get the default value of the module and assign it to Child.
Then we check the unmount status of the component and return if it has been unmounted.
Next, we set Child to the state.
In the render method, we retrieve Child from the state and use a ternary operator to check if Child is true. If it is, we render the Child component and inject this.props; otherwise, we render the placeholder.
In the componentWillUnmount method of the component, we set this.unmount to true.
Testing and Verification#
In the containers folder, create two subfolders: home and detail. In each folder, create an index.js file as the route component. The code is as follows:
home/index.js:
import React from 'react';
import ReactDOM from 'react-dom';
import { Link } from 'react-router-dom';
import '../../style.css';
import icon from '../../assets/404.png';
export default class Home extends React.Component {
render() {
return (
<div>
<h1>Hello Webpack</h1>
<img src={icon} alt=""/>
<Link to="/detail">Detail</Link>
</div>
)
}
}
detail/index.js:
import React from 'react';
import ReactDOM from 'react-dom';
import '../../style.css';
import icon from '../../assets/404.png';
export default class Detail extends React.Component {
render() {
return (
<div>
<h1>Hello Webpack Detail</h1>
<img src={icon} alt=""/>
</div>
)
}
}
In the root directory's package.json, configure the start script:
"scripts": {
"start": "webpack-dev-server --mode development",
"build": "webpack --mode production"
},
Then run npm start to start the project.
Open a browser and visit localhost:8080.
You will see that the page initially loads main.js and 0.js. Clicking the "Detail" button will navigate to http://localhost:8080/detail.
You will immediately see that 1.js is loaded. This achieves code splitting, as each route is dynamically loaded. This improves the initial rendering speed.