Webpack code splitting example

Loading time is one of the key metric for any web application these days. There are several optimization techniques exist to help with reducing page load time. If you are using Webpack, you can make use of tools that come in-built to improve page loading time.

In this article, we are going to see how to do code splitting using webpack. Let's start!

What we are building?

We are building an imaginary web application, with a landing page and button. When the user clicks on the button, we are going to load a dynamic form. In real word, this form could be your big application.

Loading a dynamic form

The problem here is, the form we are loading could be huge. We need to show this form only when the user clicks on the button.

But, Without code splitting, we are loading the entire bundle, even for the user's who may not even click the button. That's bad!

Bundle size without optimization is huge

So, let's optimize this: let's split landing page bundle and form bundle, and load the form bundle only when the user clicks on the button.

Setting up Webpack

First, we need a project and a working webpack setup. To do this, run the below commands in one of your working directory.

$ mkdir codepslit
$ cd codesplit
$ npm init
$ npm install lodash
$ npm install webpack webpack-dev-server -D

Once the dependencies are installed, open this project in any of your favorite IDE (I used IntelliJ). Now create folders src and dist that will be our source and output folders. Also, create a file named webpack.config.js in the root of your project. Finally, your structure will look something like this:

Directory Structure

Now, use the following webpack configuration to get our project built through webpack.

webpack.config.js

const path = require('path');

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

With this simple configuration, webpack takes the source files from ./src/index.js and outputs them to dist folder. Now, lets create two files: index.js - our landing page and form.js - our dynamically loaded form.

src/index.js

// statically import form module.
import form from "./form";

window.onload = function () {
    let btn = document.getElementById('load');
    btn.addEventListener('click', function () {
        document.getElementById('form').appendChild(form.render());
    });
};

In our landing page, we are statically importing form.js on load itself and attaching an event to the button. When the button is clicked, we are rendering the form contents to our page (Note: for simplicity, I am using methods like appendChild to create dynamic elements on page. For production, you should consider frameworks such as React, Vue which are really good at UI construction).

src/form.js

import _ from "lodash";

export default {
    render: function () {
        let form = document.createElement('form');
        _.map(['Name', 'Email', 'Contact'], function (field) {
            let lbl = document.createElement('label');
            lbl.innerHTML = field;
            let txt = document.createElement('input');
            txt.type = 'text';
            let container = document.createElement('div');
            container.className = 'field';
            container.appendChild(lbl);
            container.appendChild(txt);
            form.appendChild(container);
        });
        return form;
    }
};

Our imaginary application form.js is very simple: It constructs three form fields dynamically and returns them. Again, don't use appendChild or createElement to construct dynamic UI in production code. Use any frameworks.

dist/index.html

<body>
    <div class="app-loader">
        <h1>User Registration Form</h1>
        <button id="load">Load Form</button>
    </div>
    <div id="form"></div>
    <script src="/bundle.js"></script>
</body>

Above is the HTML file that houses our application. For brevity, the style part is stripped out.

With all the above files created, the final directory structure will look like this:

Final Directory Structure

Bundling our project

Now when you run webpack, you will webpack is emitting one single bundle that has both our index.js and form.js. And this single bundle weighs 545 KB which is bad and webpack itself complains about it via the comment [big].

Pre bundle size

Code splitting using Dynamic imports

Webpack supports dynamic imports via import(). With this, webpack will be able to dynamically load a bundle at run time. But in order to use this, we need to setup babel first, as dynamic imports syntax is not supported out of the box now. We also need Syntax Dynamic Import babel plugin.

1. Install Babel and Configure webpack to use babel loader for JS files.

npm install --save-dev babel-loader babel-core babel-plugin-syntax-dynamic-import
/* add to webpack.config.js */
module: {
  rules: [
    { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" }
  ]
}

2. Configure Babel to use Syntax Dynamic Import

Add a .babelrc file at the root of your project.

{
  "plugins" : ["syntax-dynamic-import"]
}

Once you have babel setup, now it's time to setup webpack to use dynamic imports. To do this, let tell webpack to emit named chunks. Modify the entry and output options in webpack.config.js as below:

entry: {
    index: "./src/index.js"
},
output: {
    path: path.resolve(__dirname, 'dist/'),
    filename: '[name].bundle.js',
    chunkFilename: '[name].bundle.js',
},

And modify our landing page index.js to use dynamic imports instead of static import.

window.onload = function () {
    let btn = document.getElementById('load');
    btn.addEventListener('click', function () {
        // dynamically import form module at run time.
        import(/* webpackChunkName: "form" */ './form').then(function (form) {
            document.getElementById('form').appendChild(form.default.render());
        });
    });
};

Notice that we have removed import form from "./form.js" line and moved it inside the event handler function of our button. We dynamically import this module via import(/* webpackChunkName: "form" */ './form') statement. This will return a Promise which will receive the dynamically loaded module as its param once its resolved. Inside the Promise callback, you can use the dynamically loaded module anyway you want.

Note: You need to access your exported values from the dynamically loaded module as module.default (earlier, it was just module). Also, in index.html, load your modules as /index.bundle.js, instead of /bundle.js.

With this setup, now if you run webpack, you will see webpack generated two modules index.bundle.js and form.bundle.js.

Code split bundles

And, if you notice, now our landing page index.bundle.js size is only 6.14 KB, which is a great reduction from the original size of 546 KB. Since we will load only the index.bundle.js upon page load, our app will be very fast on the initial load. Hurrah!

To prove this, let's measure our app performance with Lighthous for both pre and post optimization.

Comparing page load time

As you can see, with this simple setup, we are saving around 700ms of page load time. Our site loads and become interactive faster, which is a win! 🎉