A critical part for improving frontend performance is to reduce the JavaScript bundle size that should be downloaded via the network.

Even though we have faster computers and mobile devices today, we as developers should think about the audience we are building the product for as a whole. Not everybody has access to the same type of fast device or is on the fastest internet network. So we need to have a broader look at performance problems. Performance can be pursued in many different ways, but for this article, we will focus on frontend performance. We look at this aspect more closely and offer suggestions for possible improvements in this area.

Frontend performance

Frontend performance optimization is critical because it accounts for around 80-90% of user response time. So when a user is waiting for a page to load, around 80-90% of the time is due to frontend related code and assets. The below illustrations shows the ratio of frontend/backend assets to be loaded for LinkedIn.

Source: http://www.stevesouders.com/

A large part of frontend loading time is spent on executing JavaScript files as well as rendering the page. But a critical part for improving the frontend performance is to reduce the JavaScript bundle size that should be downloaded via the network. The lower the size of the JavaScript bundle, the faster the page can be available to the users.

If we look at historical data, we can see that JavaScript files were on average 2KB in 2010. But with the evolution of JavaScript, the introduction of new JavaScript libraries, like Angular or React, and with the concept of single-page applications, the average JavaScript asset size has increased to 357KB in 2016. We need to use these new technologies for better solutions. But we also need to consider possible ways for improving their performance by doing things like reducing overall JavaScript bundle size. But before diving into that topic, we need to get familiar with JavaScript bundles. What are they exactly?

JavaScript bundles

Your frontend application needs a bunch of JavaScript files to run. These files can be in the format of internal dependency like the JavaScript files you have written yourself. They can also be external dependencies and libraries you use to build your application like React, lodash, or jQuery. So in order for your page to load up the first time, these JavaScript files need to be accessible to the application. So how do we expose them?

In the past, the way of exposing JavaScript files was much more straightforward. Most of the web pages did not need many JavaScript assets. Since we did not have access to a standard way of requiring dependencies, we had to rely on using global dependencies. Imagine that we needed both jQuery as well as a main.js and other.js which hold all of our application JavaScript logic. The way we were able to expose these dependencies looked something like this:

<script src="/js/main.js"></script>
<script src="/js/other.js"></script>
<script src="//code.jquery.com/jquery-1.12.0.min.js"></script>

This was an easy solution for this problem but quickly got out of hand when scaling the application. For example, if main.js changes in a way that depends on the code in other.js, we need to reorder our script tags like this:

<script src="/js/other.js"></script>
<script src="/js/main.js"></script>
<script src="//code.jquery.com/jquery-1.12.0.min.js"></script>

As we can see, managing such a code structure at scale would quickly become a mess. But after a while, there were better solutions for managing this in applications. For example, if you were using NodeJS, you could have relied on NodeJS’s own module system (based on commonJS spec). This would allow you to use the require function for requiring dependencies. So in a Node environment, our above code snippet would look something like this:

<script>
  var jQuery = require('jquery')
  var main = require('./js/main')
  var other = require('./js/other')
</script>

Nowadays, you do not have just a couple of JavaScript files to run your application. The JavaScript dependencies for your application can include several hundred or thousands of files and it is clear listing them like the above snippet is not possible. There are several reasons for this:

  • Separating JavaScript assets into separate files does require a lot of HTTP requests when different parts of the application require different dependencies. It will not be performant and takes a lot of time
  • Additionally, NodeJS require is synchronous, but we want it to be asynchronous and to not block the main thread if the asset is not already downloaded

So the best approach seems to be putting all of the JavaScript code into a single JavaScript file and handling all the dependencies within that. Well, that is the basic job of a JavaScript bundler. Although different bundlers can have different strategies for doing this. Let us explore this a bit further and see how a bundler achieves this. Then we’ll see if there are extra improvements we can make to achieve a smaller bundle size and hence more performance. For the purpose of this article, we will use Webpack as a bundler, which is one of the most famous options out there.

Building a sample bundle with Webpack

Let’s start by setting up a simple Webpack project. We are going to use the basic packages for kickstarting a simple web application project. React, ReactDOM as UI framework, SWC as a faster alternative to Babel for transpilation, as well as a series of Webpack tools and loaders. This is what ourpackage.json will look like:

// package.json

{
  "name": "project",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "rm -rf ./dist && webpack",
    "start": "webpack-dev-server"
  },
  "keywords": [],
  "author": "",
  "license": "MIT",
  "devDependencies": {
    "@swc/core": "^1.1.39",
    "css-loader": "^3.4.0",
    "html-loader": "^0.5.5",
    "html-webpack-plugin": "^3.2.0",
    "sass-loader": "^8.0.0",
    "style-loader": "^1.1.1",
    "swc-loader": "^0.1.9",
    "webpack": "^4.41.4",
    "webpack-cli": "^3.3.10",
    "webpack-dev-server": "^3.10.1"
  },
  "dependencies": {
    "react": "^16.12.0",
    "react-dom": "^16.12.0",
    "regenerator-runtime": "^0.13.5"
  }
}

We are also going to need a [webpack.config.js](https://webpack.js.org/configuration/) which is the config entry point for our Webpack commands. There are several options in this file, but let’s clarify a few of the important ones:

  • mode— This is an option for Webpack to know if it should do any optimization, based on the option it is passed to. We will discuss this further later
  • output— This option tells Webpack where it should load or put assembled bundles at a root level. It takes both the path as well as file name
  • HTMLWebpackPlugin — This option helps us to make serving our HTML files with Webpack bundle easier
  • loaders— These loader add-ons help you transform most of the modern coding language features into understandable code for all browsers
// global dependencies
const path = require('path');
const HTMLWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  mode: "production",
  // DOC: https://webpack.js.org/configuration/output/
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: 'index_bundle.js'
  },
  // DOC: https://webpack.js.org/configuration/dev-server/
  devServer: {
    contentBase: path.join(__dirname, 'dist'),
    compress: true,
    port: 9000
  },
  module: {
    rules: [
        {
        test: /\.jsx?$/ ,
        exclude: /(node_modules|bower_components)/,
        use: {
            // `.swcrc` in the root can be used to configure swc
            loader: "swc-loader"
        }
      },
      {
        test: /\.html$/,
        use: [
          {
            loader: "html-loader",
            options: { minimize: true }
          }
        ]
      },
      {
        test: /\.scss/i,
        use: ["style-loader", "css-loader", "sass-loader"]
      }
    ]
  },
  plugins: [
    // DOC: https://webpack.js.org/plugins/html-webpack-plugin/
    new HTMLWebpackPlugin({
      filename: "./index.html",
      template: path.join(__dirname, 'public/index.html')
    })
  ]
};

Measuring and analyzing

Now, it is time to get some initial measurements in place for our Webpack build. When Webpack does the compilation, we need some sort of statistics on the built modules, compilation speed, and the generated dependency graph. Webpack already offers us the tools to get these statistics, by running a simple CLI command:

webpack-cli --profile --json > compilation-stats.json

By passing the --json > compilation-stats.json, we are telling Webpack to generate the build statistics and dependency graph as a json file with our specified name. By passing the --profile flag, we get more detailed build statistics on individual modules. After running this command, you get a json file, including a lot of useful information. But to make things easier, we are going to use a recommended tool that will visualize all of these build statistics. All you need to do is to drag the compilation-stats.json into the specified section in this official analysis tool. After doing this, we get the following results.

#react #webpack #javascript #web-development

Reducing JavaScript Bundle Size
2.65 GEEK