Photo by Brooke Lark on Unsplash

Code splitting is a technique used to dramatically decrease initial render times. You also download only as much assets (css, js, html) as you need if it is implemented correctly.

One of the most popular bundlers is Webpack and it allows code splitting in three different ways:

1. Entry points

This is the easiest and the most intuitive one used to split code. However, it is manual and has its pitfalls.

module.exports = {
entry: {
index: './src/index.js',
another: './src/another-module.js',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};

This will have the following results:

asset index.bundle.js
asset another.bundle.js
cacheable modules
./src/index.js
./src/another-module.js

It has some pitfalls as I already mentioned:

  • If multiple entry chunks use the same modules, it will be included in both bundles - duplicated.
  • It isn’t very flexible and can’t be used to split chunks automatically.

2. Prevent Duplication

The dependOn option allows to share the modules between the chunks:

module.exports = {
entry: {
index: {
import: './src/index.js',
dependOn: 'shared',
},
another: {
import: './src/another-module.js',
dependOn: 'shared',
},
shared: 'lodash',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};

The result:

asset shared.bundle.js
asset runtime.bundle.js
asset index.bundle.js
asset another.bundle.js 1.65 KiB
Entrypoint index 1.77 KiB = index.bundle.js
Entrypoint another 1.65 KiB = another.bundle.js
Entrypoint shared 557 KiB = runtime.bundle.js 7.79 KiB shared.bundle.js 549 KiB
cacheable modules 530 KiB
./node_modules/lodash/lodash.js
./src/another-module.js
./src/index.js

As you can see there’s another runtime.bundle.js file generated besides shared.bundle.js, index.bundle.js and another.bundle.js.

Although using multiple entry points per page is allowed in webpack, it should be avoided when possible in favour of an entry point with multiple imports:
entry: { page: ['./analytics', './app'] }
This results in a better optimization and consistent execution order when using async script tags.

SplitChunksPlugin

The SplitChunksPlugin allows us to extract commonly used dependencies into an existing entry chunk or an entirely new chunk. Here is an example duplicatinglodash dependency from the previous example:

module.exports = {
entry: {
index: './src/index.js',
another: './src/another-module.js',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
optimization: {
splitChunks: {
chunks: 'all',
},
},
};

Optimization.splitChunks removes duplicate dependency from our index.bundle.js and another.bundle.js. The plugin notices that we've separated lodash out to a separate chunk and remove the dead weight from our main bundle.

The build result:

asset vendors-node_modules_lodash_lodash_js.bundle.js
asset index.bundle.js
asset another.bundle.js
Entrypoint index 558 KiB = vendors-node_modules_lodash_lodash_js.bundle.js 549 KiB index.bundle.js 8.92 KiB
Entrypoint another 558 KiB = vendors-node_modules_lodash_lodash_js.bundle.js 549 KiB another.bundle.js 8.8 KiB
cacheable modules
./src/index.js
./src/another-module.js
./node_modules/lodash/lodash.js

Another useful plugin for splitting code is mini-css-extract-plugin. It is useful for splitting CSS out from the main application.

3. Dynamic Imports

The recommended approach is to use the dynamicimport() syntax.

module.exports = {
entry: {
index: './src/index.js',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};

Instead of statically importing lodash, we'll use dynamic importing to separate a chunk:

function getComponent() {
async function getComponent() {
const element = document.createElement('div');
const { default: _ } = await import('lodash');

element.innerHTML = _.join(['Hello', 'webpack'], ' ');

return element;
}

getComponent().then((component) => {
document.body.appendChild(component);
});

Build result shows uslodash is separated out to a separate bundle:

asset vendors-node_modules_lodash_lodash_js.bundle.js
asset index.bundle.js
cacheable modules
./src/index.js
./node_modules/lodash/lodash.js

Additional remark

If you’re using create-react-app it supports code splitting by default. The most common place you could split your code and reduce load time is where you define your Routes.

import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';

const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));

const App = () => (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/about" component={About}/>
</Switch>
</Suspense>
</Router>
);

Suspense component is the fallback which is rendered while your route is not loaded yet.

Thanks for reading!