How to Reduce Your Webpack Bundle Size for Web App Optimization 02

List Of Content

ADS Area (CARBON)

ADS Area (CARBON)

 

Overview 

If you have been in Web Development for a quite some time for now especially as a Front-End Developer and you use Webpack for bundling your application's code (React) into a single loadable bundle for serving your application full browser-supported.

But in cases you work on large apps that includes hundreds of components and routes and you use the default Webpack Configuration to bundle and build the main bundle plus its asset you will find problems when it comes to the bundle size, it will huge (like 4MB - 6MB) and that for a Web App that gets delivered from your server to thousands of people at the same time, Well it is not good idea at all.

So I'm gonna try to give you some tips used by teams working on large apps to reduce the size of their main bundle but you will find your self-splitting your code over multiple bundles depending on the structure of your app.

If you're new to Webpack and want to learn how to configure your own build environment you can watch this Tutorial.

So consider adopting the ways that only works along with your code base and app structure, for ex: for React apps it may differ.

Here is the Demo Project we are going to work on.

Analyze Your Project (Webpack Bundle)

So when it comes to reducing the bundle size you have to understand how your code is being bundled and compiled together using Webpack and what dependencies are included plus which modules are being used and info about each module to be able to recognize your code base and see the flows and what dependencies are being included but are not being used or only part of the module is used.

There is a lot of Webpack plugins and modules that allow you to analyze but we are going to use webpack-bundle-analyzer specifically since it provides a simple User Interface and easy to understand in order to deeply see through you bundle.

npm install --save-dev webpack-bundle-analyzer

Make sure to clone the project from the Github repo provided above, However you can use your own project you want to try to reduce its final bundle size.

Go under webpack's config file and add the analyzer plugin.

const path = require("path");
//Webpack Analyzer
const WebpackBundleAnalyzer = require("webpack-bundle-analyzer")
  .BundleAnalyzerPlugin;
//Is it in development mod
let devMode = process.env.devMode || true;

module.exports = {
  entry: path.resolve("./app.js"),
  mode: devMode ? "development" : "production",
  output: {
    filename: "app.js",
    path: path.resolve("./dist"),
    chunkFilename: "[name].js" ///< Used to specify custom chunk name 
  },
  resolve: {
    extensions: [".js", ".json"]
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/, ///< using babel-loader for converting ES6 to browser supported javascript
        loader: "babel-loader",
        exclude: [/node_modules/]
      }
    ]
  },
 //Add Analyzer Plugin alongside your other plugins...
 plugins: [
   new WebpackBundleAnalyzer()
]
};

After adding the plugin just run the Webpack build.

npm run build 

You will notice a new browser window gets opened with the analyzer localhost server running and your bundle visualized. However, you can access the Webpack Analyzer server after running the build command on http://127.0.0.1:8888/ 

Just opening the server page you will notice containers with a text inside them where each container represents a javascript (Module, folder, or package) you also notice your node_module packages on there and the bigger the size of the container gets the more larger it is.

From there you need to analyze each module and see what dependencies it has it works like a tree so each dependency comes after it's callee module and so on and so forth.

On the left side, you find your imported module (third party node_modules packages) while on the right side there is your app source code and scripts.

Try to figure out what modules are taking large space on your bundle and try to link them with your app to see if they actually do something or you're blindly importing something that you don't need or even don't use anyway, afterward try to fix it on your source code.

In our Demo app, we are using the find function from Lodash set of function and we are including all the functions and letting Webpack adding them to our bundle without even noticing, therefore, we can just import the find function instead offset of functions.

//So, instead of 
import * as _ from "lodash";
//We only need the find function
import { find } from "lodash";

If you try to fix your dependencies and imports and running the build again you should notice a difference on the bundle size between now and before.

Split Code Chunks (Vendor)

Splitting you Code into two or more bundles is very necessary when it comes to a full dedicated application that is so large and has a lot of routes, pages, features, and nested User Interfaces if you skipped this part and tried to put everything in one single bundle that will be very expensive when it comes server loading and user experience and your app performance overall.

You simply use Code Splitting technique to split code that is dependent from each other into different chunks (bundles) and use them whenever the user tried to use a specific functionality or even access a router on your apps, this helps a lot when it comes to single page apps and React apps.

On our Demo Project we are using the find function dependency from Lodash module (set of functions) and let's say that this function is only going to be needed when the user tries to get a list of available Front-End Frameworks but it is not necessary to load it at Web App startup and initialization. The simple solution is to split your app's code into two bundles (main bundle and vendor bundle or chunk) So your app main functionality will be separated from another third party function, also, it is better to load your app with a smaller bundle while loading other vendor's bundle after initialization.

Go into your Webpack main configuration and make sure you are using Webpack v4 or higher since the API changed from the previous versions.

//Other Config 
...
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/, ///< put all used node_modules modules in this chunk
          name: "vendor", ///< name of bundle
          chunks: "all" ///< type of code to put in this bundle
        }
      }
    }
  },
...

Using the Split Chunks configuration you can customize which modules get added to a separate chunk bundle and gets separated from the rest of the main bundle which is mainly should be for your initial app code.

Also, Add the chunksName configuration to the output section on webpack.config.js to customize the name of the custom chunks bundle's name.

output: {
  filename: "app.js",
  path: path.resolve("./dist"),
  chunkFilename: "[name].js" ///< Custom name for Chunks [name] represents the name of the module
},

Now try to run the build.

//The Output should be simillar to
Hash: 220096268cc0847a202c
Version: webpack 4.26.0
Time: 1140ms
Built at: 2018-11-23 22:51:33
    Asset      Size  Chunks             Chunk Names
   app.js  8.61 KiB    main  [emitted]  main
vendor.js   547 KiB  vendor  [emitted]  vendor
Entrypoint main = vendor.js app.js
[./app.js] 27 bytes {main} [built]
[./node_modules/webpack/buildin/global.js] (webpack)/buildin/global.js 489 bytes {vendor} [built]
[./node_modules/webpack/buildin/module.js] (webpack)/buildin/module.js 497 bytes {vendor} [built]
[./src/bootstraper.js] 130 bytes {main} [built]
[./src/utils/index.js] 530 bytes {main} [built]
    + 1 hidden module

You should get on the assets section two bundles (your main app.js bundle) and (then new vendor bundle).

Make sure to link the new vendor bundle that Webpack generated for you to be able to use your third party modules functions.

<script src="./app.js"></script>
<script src="./vendor.js"></script>

You can Now try to access your app through a simple HTTP Server and open the Dev Tools Network tab to see the time it takes to load the app.js & vendor.js bundle and how it is loading faster.

Load Code Dynamically (Asynchronously)

This part is so efficient when you have some parts of your app that only used on a specific scenario and not like on the app startup, let's say you have a registration system and of course users won't access that when accessing your web app but only needed when the client tries to register therefor there is no need to include it on the initial app bundle but what you can do is create a separated bundle for it that gets loaded Asynchronously in the background when the user asks for the registration page.

And not only that, you can use it anywhere on your app when it comes to the possibility of lazily loading code.

We will use the Experimental ES6 & Webpack Async import syntax that allows you to import either local or node modules only when needed without adding them to the main app bundle but adding them into a separate bundle that handles all the async code needed and since Webpack is smart enough to know which module is requested therefore it won't load the whole bundle but only a portion of it.

First, make sure to install babel dynamic syntax plugin only if you are using a plugin but if you are using other loaders this depends on your loader so it's better to go with Babel.

npm install @babel/plugin-syntax-dynamic-import --save-dev 

And add it to the .babelrc configuration

/* .babelrc */
{
  "presets": [],
  "env": {
    "development": {
      "plugins": ["@babel/syntax-dynamic-import"]
    }
  }
}

In our Demo App Case, we will use Async import to require the find function from Lodash package only when needed, but this doesn't represent thactualll use case of this method.

//Don't use import {find} from "lodash" since we will do it Asynchronously
export function getFrontEndFrameworks() {
  const frameworks = [
    { name: "React", type: "front-end" },
    { name: "Angular", type: "front-end" },
    { name: "Vue", type: "front-end" },
    { name: "Express", type: "back-end" }
  ];
  let frontEnd = [];
  //Now We need the find function let's load it Dynamically 
  import("lodash").then(({ find }) => {
   //import() is a function that returns a Promise gets resolved with the Request Module
   //We only import { find } function from the "lodash" module
   //Then use it normally
   //NOTICE: is you use import() you need to refactor your code to support Async Work Flow
    find(frameworks, (fw, idx) => {
      if (fw.type == "front-end") {
        frontEnd.push(fw.name);
        console.log("Front-End Framework: ", fw.name);
      }
    });
    return frameworks;
  });
}

As you can see instead of importing the find function from Lodash at the top of the module (which gets loaded on the startup of the application) we tried to import it dynamically using an Async call. Webpack will do all the heavy handling for us behind the scenes so you don't have to worry how the Request is going to be or where this is all being handled by our great friend Webpack.

Remember on the split chunks vendor configuration we set the chunks to all (which includes support for all the types among this is the async) of course you can choose async only instead of all to only include dynamically imported code on the bundle instead of all type of chunks.

Now run the build, you should see some difference in the sizes now and before using the dynamic import since some of your functions are being loaded Asynchronously only when needed instead of on app startup.

You won't notice the difference when it comes to your app since Webpack is going to handle the request and module fetch dynamically behind the scenes but this is very efficient.

 

Compressing Bundles 

Compression can save you a lot of load time and bundle size since it replaces your Human-readable javascript code with some included data in a specific algorithm most of the time the GZIP Algorithm is used in the process of compressing Webpack bundles and use them with the browser.

But this workaround has its drawback too since not all the browsers support loading and decoding compressed javascript bundles and for setting this with your Web server that is responsible for delivering you compressed app bundle is a bit of pain in the ass so you must have a strong knowledge when it comes to dealing with Headers and accepting compression.

There are two types of Algorithm mostly used for compressing Webpack bundles and delivering to your clients:

  • GZIP, it is a Good compression Algorithm you can use compression-webpack-plugin to compress your bundles into (for ex: vendor.js.gz)
  • Brotil, Google Algorithm you can use brotli-webpack-plugin to compress your bundles (for ex: vendor.js.br)

You can try to search on how to use this with your specific framework or Web Server but you can check express-static-gzip for serving gzip & Brotil compressed bundles from an Express Web Server.

 

What's Next

Of course, those are not all the ways you can find out there for reducing Webpack's bundle size but by far those what I find most efficient and helps a lot with big bundles and large apps espicially single page apps that use React.

For Compression it's a bit tricky and not fully supported by all the browsers which leaves you with some drawbacks on your App's bundle delivery.

Share Tutorial

Made With By

Ipenywis Founder, Game/Web Developer, Love Play Games