How to reduce javascript bundle size with webpack code splitting 2020

How to reduce javascript bundle size with webpack code splitting 2020

Welcome to Nairaguide. Today, we will be focusing on “how to reduce your javascript bundle size with webpack code splitting approach”.

With the emergence of frameworks Such as AngularJS and React js coupled with the popularization of Single Page Apps, usage of JavaScript has exploded on the front end.  With the broad range of JavaScript libraries and frameworks used to build up these web programs, the means to reduce the magnitude of JavaScript bundles has never been important.

In the piece, we will be looking at:

  • what a javascript bundle is
  • what is code splitting
  • webpack code-splitting library

let’s get started!

 

Javascript bundles

Let’s first talk about JavaScript bundles. Bundles typical refer to JavaScript and CSS bundles delivered from the server to the consumer’s browser. When we discuss it the principal focus is the size of the JavaScript package sent to initialize the internet app, and also that package impacts the quantity of time before a consumer can use the program. However, what is the main package?

There is a time once we would send a few JavaScript files to create our webpage more interactive. More frequently than not, you would have a CDN link to JQuery, maybe some plugins, then a main.js which held most of your programming. This was not a hard-fast guideline, but in such a situation, you had one file with JavaScript. The JavaScript will be sent to the browser with all the HTML and CSS once a user left a request to the server. The actual webpage and content were often entirely constructed on the server prior to being delivered to the client. In case the user needed the next page, they made the following petition to this server.

That’s not to say minification and size were not essential at the moment!

The landscape is different. Single Page Applications (SPA’s) mean that an individual is likely to create a request to a server and download the entire web app. From that point, JavaScript will care for routes, interactions, network requests, and more. That means there’s a lot more JavaScript than ever before. Furthermore, our projects have gone out of a couple of .js documents to thousands or hundreds. When we were to request all these separately, it would take a lifetime!

We use tools such as Webpack, Parcel, or Rollup to shoot our documents and package them into bundles to get supply. The default strategy of those systems that are bundling is to create one bundle out of ours. As this may take the least amount of time and the amount of requests to accumulate the information or files. Naturally, these programs do more than that, except for our purpose, this created package is our main bundle. To view that in action, we can make a new create-react-app.

npx create-react-app bundle-test
cd bundle-test
npm run build

We’re using create-react-app because it’s a pure boilerplate to visualize changes, without having to step in the code!

reduce javascript bundle

We will get into what these other “chunks” are later in the series; we’re just interested in the main.#.chunk.js. This chunk is the collection of all our components packaged into a single file by webpack. Any new code we write will be added to this chunk and downloaded as a single bundle.

However, why do we care about the size of the bundle?

About code splitting with Webpack

In this brief step by step tutorial, I want to explain how to use Webpack Code Splitting to reduce your JavaScript library’s bundle size, how to export JavaScript code from relative paths from your library, and how to import these fragments in your actual JavaScript project using your library.

Code splitting is one of the most compelling features of webpack. This feature allows you to split your code into various bundles which can then be loaded on demand or in parallel. It can be used to achieve smaller bundles and control resource load prioritization which, if used correctly, can have a major impact on load time.

There are three general approaches to code splitting available:

  • Entry Points: Manually split code using entry configuration.
  • Prevent Duplication: Use the SplitChunksPlugin to dedupe and split chunks.
  • Dynamic Imports: Split code via inline function calls within modules.

Entry Points

This is by far the easiest and most intuitive way to split code. However, it is more manual and has some pitfalls we will go over. Let’s take a look at how we might split another module from the main bundle:

project

webpack-demo
|- package.json
|- webpack.config.js
|- /dist
|- /src
  |- index.js
+ |- another-module.js
|- /node_modules

another-module.js

import _ from 'lodash';

console.log(
  _.join(['Another', 'module', 'loaded!'], ' ')
);

webpack.config.js

const path = require('path');

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

This will yield the following build result:

...
            Asset     Size   Chunks             Chunk Names
another.bundle.js  550 KiB  another  [emitted]  another
  index.bundle.js  550 KiB    index  [emitted]  index
Entrypoint index = index.bundle.js
Entrypoint another = another.bundle.js
...

As mentioned there are some pitfalls to this approach:

  • If there are any duplicated modules between entry chunks they will be included in both bundles.
  • It isn’t as flexible and can’t be used to dynamically split code with the core application logic.

The first of these two points is definitely an issue for our example, as lodash is also imported within ./src/index.js and will thus be duplicated in both bundles. Let’s remove this duplication by using the SplitChunksPlugin.

Prevent Duplication

The SplitChunksPlugin allows us to extract common dependencies into an existing entry chunk or an entirely new chunk. Let’s use this to de-duplicate the lodash dependency from the previous example:

The CommonsChunkPlugin has been removed in webpack v4 legato. To learn how chunks are treated in the latest version, check out the SplitChunksPlugin.

webpack.config.js

  const path = require('path');

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

With the optimization.splitChunks configuration option in place, we should now see the duplicate dependency removed from our index.bundle.js and another.bundle.js. The plugin should notice that we’ve separated lodash out to a separate chunk and remove the dead weight from our main bundle. Let’s do an npm run build to see if it worked:

...
                          Asset      Size                 Chunks             Chunk Names
              another.bundle.js  5.95 KiB                another  [emitted]  another
                index.bundle.js  5.89 KiB                  index  [emitted]  index
vendors~another~index.bundle.js   547 KiB  vendors~another~index  [emitted]  vendors~another~index
Entrypoint index = vendors~another~index.bundle.js index.bundle.js
Entrypoint another = vendors~another~index.bundle.js another.bundle.js
...

Here are some other useful plugins and loaders provided by the community for splitting code:

Dynamic Imports

Two similar techniques are supported by webpack when it comes to dynamic code splitting. The first and recommended approach is to use the import() syntax that conforms to the ECMAScript proposal for dynamic imports. The legacy, webpack-specific approach is to use require.ensure. Let’s try using the first of these two approaches…

import() calls use promises internally. If you use import() with older browsers, remember to shim Promise using a polyfill such as es6-promise or promise-polyfill.

Before we start, let’s remove the extra entry and optimization.splitChunks from our config as they won’t be needed for this next demonstration:

webpack.config.js

  const path = require('path');

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

Note the use of chunkFilename, which determines the name of non-entry chunk files. For more information on chunkFilename. We’ll also update our project to remove the now unused files:

project

webpack-demo
|- package.json
|- webpack.config.js
|- /dist
|- /src
  |- index.js
- |- another-module.js
|- /node_modules

Now, instead of statically importing lodash, we’ll use dynamic importing to separate a chunk:

src/index.js

- import _ from 'lodash';
-
- function component() {
+ function getComponent() {
-   const element = document.createElement('div');
-
-   // Lodash, now imported by this script
-   element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+   return import(/* webpackChunkName: "lodash" */ 'lodash').then(({ default: _ }) => {
+     const element = document.createElement('div');
+
+     element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+
+     return element;
+
+   }).catch(error => 'An error occurred while loading the component');
  }

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

The reason we need default is that since webpack 4, when importing a CommonJS module, the import will no longer resolve to the value of module.exports, it will instead create an artificial namespace object for the CommonJS module. For more information on the reason behind this, read webpack 4: import() and CommonJs

Note the use of webpackChunkName in the comment. This will cause our separate bundle to be named lodash.bundle.js instead of just [id].bundle.js. For more information on webpackChunkName and the other available options, see the import() documentation. Let’s run webpack to see lodash separated out to a separate bundle:

...
                   Asset      Size          Chunks             Chunk Names
         index.bundle.js  7.88 KiB           index  [emitted]  index
vendors~lodash.bundle.js   547 KiB  vendors~lodash  [emitted]  vendors~lodash
Entrypoint index = index.bundle.js
...

As import() returns a promise, it can be used with async functions. However, this requires using a pre-processor like Babel and the Syntax Dynamic Import Babel Plugin. Here’s how it would simplify the code:

src/index.js

- function getComponent() {
+ async function getComponent() {
-   return import(/* webpackChunkName: "lodash" */ 'lodash').then(({ default: _ }) => {
-     const element = document.createElement('div');
-
-     element.innerHTML = _.join(['Hello', 'webpack'], ' ');
-
-     return element;
-
-   }).catch(error => 'An error occurred while loading the component');
+   const element = document.createElement('div');
+   const { default: _ } = await import(/* webpackChunkName: "lodash" */ 'lodash');
+
+   element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+
+   return element;
  }

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

It is possible to provide a dynamic expression to import() when you might need to import specific module based on a computed variable later.

Prefetching/Preloading modules

webpack 4.6.0+ adds support for prefetching and preloading.

Using these inline directives while declaring your imports allows webpack to output “Resource Hint” which tells the browser that for:

  • prefetch: resource is probably needed for some navigation in the future
  • preload: resource might be needed during the current navigation

Simple prefetch example can be having a HomePage component, which renders a LoginButton component which then on demand loads a LoginModal component after being clicked.

LoginButton.js

//...
import(/* webpackPrefetch: true */ 'LoginModal');

This will result in <link rel="prefetch" href="login-modal-chunk.js"> being appended in the head of the page, which will instruct the browser to prefetch in idle time the login-modal-chunk.js file.

webpack will add the prefetch hint once the parent chunk has been loaded.

Preload directive has a bunch of differences compared to prefetch:

  • A preloaded chunk starts loading in parallel to the parent chunk. A prefetched chunk starts after the parent chunk finishes loading.
  • A preloaded chunk has medium priority and is instantly downloaded. A prefetched chunk is downloaded while the browser is idle.
  • A preloaded chunk should be instantly requested by the parent chunk. A prefetched chunk can be used anytime in the future.
  • Browser support is different.

Simple preload example can be having a Component which always depends on a big library that should be in a separate chunk.

Let’s imagine a component ChartComponent which needs huge ChartingLibrary. It displays a LoadingIndicator when rendered and instantly does an on demand import of ChartingLibrary:

ChartComponent.js

//...
import(/* webpackPreload: true */ 'ChartingLibrary');

When a page which uses the ChartComponent is requested, the charting-library-chunk is also requested via <link rel="preload">. Assuming the page-chunk is smaller and finishes faster, the page will be displayed with a LoadingIndicator, until the already requested charting-library-chunk finishes. This will give a little load time boost since it only needs one round-trip instead of two. Especially in high-latency environments.

Using webpackPreload incorrectly can actually hurt performance, so be careful when using it.

Bundle Analysis

Once you start splitting your code, it can be useful to analyze the output to check where modules have ended up. The official analyze tool is a good place to start. There are some other community-supported options out there as well:

  • webpack-chart: Interactive pie chart for webpack stats.
  • webpack-visualizer: Visualize and analyze your bundles to see which modules are taking up space and which might be duplicates.
  • webpack-bundle-analyzer: A plugin and CLI utility that represents bundle content as a convenient interactive zoomable treemap.
  • webpack bundle optimize helper: This tool will analyze your bundle and give you actionable suggestions on what to improve to reduce your bundle size.
  • bundle-stats: Generate a bundle report(bundle size, assets, modules) and compare the results between different builds

 

 

Summary:Webpack Code Splitting for your

Library

In your current application, you may have the following or a similar Webpack configuration with just a single entry point:

module.exports = {
entry: ‘./src/index.js’,
output: {
path: `${__dirname}/lib`,
filename: ‘index.js’,
library: ‘my-library-name’,
libraryTarget: ‘umd’,
},
};

Furthermore, in your package.json file you may have the following or a similar key/value pair for the main entry point of your library:

{
“name”: “my-library-name”,
“version”: “1.0.0”,
“description”: “”,
“main”: “lib/index.js”,
}

Having one single entry point to your library is fine until your library’s bundle size grows beyond a certain threshold. Eventually it will have negative side-effects importing your whole library into your JavaScript application, considering that you don’t need all parts of your library at once, because it slows down the initial workload of your application.

Let’s see how we can use Code Splitting to our advantage. First, we will use multiple entry points instead of a single entry point:

module.exports = {
entry: {
main: ‘./src/index.js’,
add: ‘./src/add.js’,
subtract: ‘./src/subtract.js’,
},
output: {
path: `${__dirname}/lib`,
filename: ‘[name].js’,
library: ‘my-library-name’,
libraryTarget: ‘umd’,
},
}

While /src/index.js exports the functions from /src/add.js and /src/add.js to bundle it still as the whole library in the main entry point, both functions get bundled themselves for their add and subtract entry points respectively.

{
“name”: “my-library-name”,
“version”: “1.0.0”,
“description”: “”,
“main”: “lib/main.js”,
}

In the package.json file, we change the entry point to our whole library to the new entry point which bundles our whole library with one of our Webpack entry points. However, since we have new entry points for our single JavaScript functions, we can import them as standalone functionalities to our JavaScript application — which installs and uses our library — now.

 

// imports whole library
import { add, subtract } from ‘my-library-name’;
// imports whole library too
// because the *src/index.js* from the library imports/exports subtract function
import { add } from ‘my-library-name’;
// imports add as standalone function
// without import whole library
import add from ‘my-library-name/lib/add’;
// imports subtract as standalone function
// without import whole library
import subtract from ‘my-library-name/lib/subtract’;

That’s it for Code Splitting a JavaScript library with Webpack. If you don’t need all parts of your library, then Code Splitting helps you to avoid importing the whole library but using only parts of it instead.

 

 

 

https://developers.google.com/web/fundamentals/performance/prpl-pattern/

https://developers.google.com/web/fundamentals/performance/user-centric-performance-metrics

https://developers.google.com/web/tools/lighthouse/audits/time-to-interactive

https://developers.google.com/web/fundamentals/performance/why-performance-matters/

https://developers.google.com/web/fundamentals/performance/optimizing-javascript/code-splitting/

also check out java 9 tutorials for beginners in nairaguide

Java 9 tutorial for beginners

nairaguide

just a random guy, love writing, gaming, and game development

Leave a Reply

Your email address will not be published. Required fields are marked *