F

“Optimizing Single-Page Applications: Top 3 Approaches”

In this tutorial we’ll show you three great approaches to optimizing your single page applications and making it work extremely fast using Webpack.

With the growing trend toward single-page applications, server-side rendering has started to lose its popularity. It remains mostly as a service since all the logic is moving to the frontend. This approach is very popular nowadays, as it helps developers to design an understandable architecture and keep it clear with the ability to extend and support it. But nothing is perfect.

One of the major disadvantages of SPAs is slow loading speeds. A single bundle file with an entire application takes time to load for the first time (especially for big applications), and since nobody likes to wait, you could lose customers over it. But this actually isn’t a problem anymore with Webpack. Webpack can be configured pretty flexible, allowing you to get rid of this issue of slow loading in most cases. In this tutorial, We’ll use Webpack 3.11.0.

Read our case study Photoblog and find out how we optimized website performance and increase loading page speed.

Ways to speed up applications by splitting bundles into several parts with Webpack

1. Use CommonsChunkPlugins

This plugin, provided by Webpack out of the box, allows you to find and store commonly used scripts in a single file. It’s extremely useful for both single-page and multi-page applications.

CommonsChunkPlugins finds and cuts out commonly used scripts, splitting your scripts and producing a common.js file to make the whole application lighter.


Let’s imagine we have the following project:

Here we have index.js, profile.js, and user.js. Let’s say that index.js and profile.js require user.js.

We want index.js and profile.js to work separately on different pages. Ordinarily, we would write something like this:

const webpack = require('webpack');

module.exports = {

    context: __dirname + '/scripts',

    entry: {
        index: './index',
        profile: './profile'
    },

    output: {
        path: __dirname + '/app/public',
        filename: '[name].js'
    }

};

This will produce the following structure in the public folder:

If user.js is required in both index.js and profile.js, with this configuration Webpack will insert user.js into both files and the code in the final application will be duplicated, which isn’t good. We want to get commonly used parts into a separate file so the browser is able to load and cache the file.

Let’s optimize this with CommonsChunkPlugins. Our Webpack configuration for this purpose will be:

const webpack = require('webpack');

module.exports = {

    context: __dirname + '/app/scripts',

    entry: {
        index: './index.js',
        profile: './profile.js'
    },

    output: {
        path: __dirname + '/app/public',
        filename: '[name].js'
    },

    plugins: [
        new webpack.optimize.CommonsChunkPlugin({
            name: 'common'
        })
    ]

};

Let’s check what has changed:

Now you can see that a common.js file has appeared. This file contains every common require() for all of our scripts. Also, the size of files has been reduced drastically.

Of course, a real project will be more complex, so when you use different libraries for different parts of the application you’ll find this plugin extremely useful.

It’s also pretty configurable, and you can use it multiple times in the same app for any purpose.

https://webpack.js.org/plugins/commons-chunk-plugin/

2. Dynamic require()

It might be a bad idea to load all of your functionality whether or not it will be used. We prefer to load things only upon request. When your application becomes very large, you should definitely consider splitting your scripts and loading parts when they’re requested. Before automation in JavaScript, there was a need to develop our own methods for inserting new files with the <script> tag into the document dynamically. Fortunately, Webpack can now serve this purpose, and it’s pretty automatic. But remember, the ordinary require() doesn’t allow you to load files based on rules. Webpack will simply add it to the bundle or remove it all.

So if you write something like:

const location = window.location.pathname;

switch(location) {

    case 'user':
        const user = require('./user');
        user();
        break;

    case 'profile':
        const profile = require('./profile');
        profile();
        break;

    default: alert('No match'); break;

}

You might expect that files will load dynamically on request(), but they won’t. Instead, this will create a single file with all the required scripts regardless of the rules because Webpack won’t analyze the switch and the rules themselves.

Fortunately, for this purpose Webpack supplies an original require with ensure() function, which has the following syntax:

const location = window.location.pathname;

switch(location) {

    case 'user':
        require.ensure(['./user'], function(require) {
            const user = require('./user');
            user();
        });
        break;

    case 'profile':
        require.ensure(['./profile'], function(require) {
            const profile = require('./profile');
            profile();
        });
        break;

    default: alert('No match'); break;

}

After assembly we get:

Webpack creates chunk files 0.js and 1.js. Main.js will not contain these files, and they’ll be loaded only upon request—in our case, if the filename matches the URL. Since they’re on the server, don’t forget to specify publicpath for the output parameter in config.

We’ll also describe the situation for React developers who use the Router library to manage rendering. It would be great to load a file with components if they match some part of the URL. The problem is that there’s no convenient way to use require.ensure() in <Route> component. For this purpose, we use react-loadable (https://github.com/jamiebuilds/react-loadable). With this library, your routes will look like this:

<Router>
    <Route path="/profile" render={() => {
        const Profile = Loadable({
            loader: () => import('./profile'),
            loading: Loading
    });
        return <Profile/>;
    }}/>
</Router>

Also, sometimes you might like to write something like this:

const location = window.location.pathname;

require.ensure(['/somepath/' + location], function(require) {
    const script = require('/somepath/' + location);
    script();
});

This case is called a dynamic require. Since the filename is stored in a variable, it’s too complicated for Webpack to detect that file and create chunks. You’ll need to create an intermediate file with static require.ensure() or use bundle-loader. We prefer the second option: https://github.com/webpack-contrib/bundle-loader

3. Analyze your build

In this section, we’ll show you (or remind you) how to use Webpack tools for analysis.

First, use the key --display-modules. This will display information about the way your build stores modules. If we use it on our previous example with CommonsChunkPlugins, it will show the following:

You can see that:

profile.js went to the {0} chunk, which is the profile.js entry

index.js went to the {1} chunk, which is index.js (also a separate entry)

user.js went to the {2} chunk, which is common.js because this script is used by both index.js and profile.js

But you can go even deeper with -display-modules --verbose. This command will also show you why the build was assembled like this.

Here’s detailed information on why we got common.js like this and what it contains.

We always use this command to keep track of our build and be sure that it’s as small as possible and doesn’t contain duplications.

For most cases this is enough. But for really big projects, where you get a hundred of modules, it can be annoying to read everything from the console and have to analyze the entire application with all dependencies. Luckily, for this purpose Webpack has an analyzer with a graphical interface.To use it, we need to create a bulk report in JSON format first:

To use it, we need to create a bulk report in JSON format first:

This report.json file contains all necessary information about your build.

Next, go to http://webpack.github.io/analyse and upload report.json.

Finally you’ll get this:

This tool will show you information about modules, chunks, assets, warnings, and errors.

The analysis helps you to avoid duplication and keep everything working as expected.

We also welcome you to visit our case studies page. There, you can see how we implement our technical solutions to solve business problems. You can also refer to our sales team with all relevant questions.