DevTools.tech

Optimizing your JavaScript Bundle | DefinePlugin Webpack

June 24, 2019

In this blogpost, we are going to talk about webpack’s DefinePlugin and how we can use it to optimize our JS bundle by conditionally loading modules when required.

What is DefinePlugin?

DefinePlugin is provided out of the box by Webpack. As per the documentation,

The DefinePlugin allows you to create global constants which can be configured at compile time.

In our webpack config —

new webpack.DefinePlugin({
	PRODUCTION: JSON.stringify(true)
});

Here we define a constant which would be available in the global scope of our bundle —

if (PRODUCTION) {
	console.log('Production');
}

Webpack will replace the PRODUCTION constant with the value provided in the config (here it is true). Every key provided in the DefinePlugin will be available as a constant in the bundle and values must be converted to string or webpack will stringify them for you. You can read more about the rules of identifiers here.

Problem Usecase

Most of the time, we setup our build process and ship a bundle to the client. We can do all sort of fancy things like code-splitting, tree shaking, uglification and much more. However, we might ship features that aren’t required or might be experimental in nature. In such cases, we are pushing more code than required and unnecessarily increasing our bundle size. Here, DefinePlugin really shines and can help us avoid such circumstances.

For demo purposes, let’s consider an usecase. Suppose, our system offers the following features

  1. Reporting
  2. Payment
  3. A/B Testing
  4. Click Tracking
  5. Experimental Feature 1
  6. Experimental Feature 2

However, not all our clients need all the features. Different clients might require different feature set depending upon the their needs, such as

  1. Client A might require Reporting and Payment features.
  2. Client B might require Reporting, A/B Testing, Click Tracking.
  3. Client C might want to try Experimental Features.
  4. Client D might require all features.

Hence, we can’t ship all the features to all the clients. Doing so, would not be the optimial solution. Let’s see how DefinePlugin can help us in solving this problem.

Code Time

You can find code for this blogpost here.

We will setup the following directory structure

|-- dist
	|-- bundle.js
|-- src
	|-- configs
		|-- config-1.json
		|-- config-2.json
		|-- config-3.json
		..
	|-- index.js
|-- main.js

Let’s briefly look at what some of the file/folder means

  1. main.js

    • This file will contain all the code which would be required for generating our bundle.
  2. src

    • This folder will contain all the client side code we need to ship.
  3. src/index.js

    • This file will be the entry point to our bundle.
  4. configs

    • This folder will contain all the config files which will dictate what goes into our bundle and what not.

Create a config-1.json which would all us to include all the modules in our bundle. Different config files will dictate different bundles depending upon which features are enabled or disabled.

cd src/configs
touch config-1.json

Put the following code in the file

{
	"REPORTING": true,
	"PAYMENT": true,
	"A_B_TESTING": true,
	"CLICK_TRACING": true,
	"EXPERIMENTAL_FEATURE_1": true,
	"EXPERIMENTAL_FEATURE_2": true
}

Let’s start working on our bundle generation. We are going to run webpack as part of a node service which would be a little different from traditionally creating webpack.config.js and running webpack command from cli.

Open the main.js and paste the following code:

const Promise = require('bluebird');
const path = require('path');
const fs = Promise.promisifyAll(require('fs'));
const webpack = require('webpack');

const CONFIG_PATH = './src/configs/config-1.json';

function init() {
	return fs
		.readFileAsync(CONFIG_PATH, { encoding: 'utf8' })
		.then(content => {
			...
		});
}
init();

In the above code snippet, we are setting up all the required dependencies and reading the config file. This config file can be generated via any means as in read from a db or some pre-processing mechanism which generates this config.

Now, let’s look at our webpack config

	...
	.then(content => {
			return new Promise((resolve, reject) => {
				const config = JSON.parse(content);

				if (!config || !Object.keys(config).length) return reject('Empty Config Found');

				const compiler = webpack({					mode: 'production',
					entry: path.join(__dirname, 'src', 'index.js'),
					output: {
						path: path.join(__dirname, 'dist'),
						filename: 'bundle.js'
					},
					module: {
						rules: [
							{
								test: /.jsx?$/,
								loader: 'babel-loader',
								exclude: /node_modules/
							}
						]
					},
					plugins: [new webpack.DefinePlugin({ ...config })]				});
				new webpack.ProgressPlugin().apply(compiler);
				compiler.run(err => {
					return err ? reject(err) : resolve();				});
			});
	})
	...

In the above code snippet,

  1. We are creating a compiler instance by passing the standard config to webpack function.
  2. We are defining global constants as discussed earlier by using the DefinePlugin and passing the config read using file interface (‘/configs/config-1.json’).
  3. We are applying ProgressPlugin to our webpack instance to get a clear picture of what’s happening and then calling the run method to start bundle creation.
  4. Run method takes a callback which has two parameters error and stats. We are only concerned with error parameter for now.
  5. If any error occurs while creating the bundle then we will reject else resolve the promise.

We are here using Node.js API provided by webpack. It is useful in our scenario as it provides us more granular control over the build pipeline. You can read more about the Webpack’s Node Interface here.

Now, I am going to add dummy modules so that we can run our build pipeline and compare the results. You can checkout the github repo for the code.

Conditional Module Loading

Open the src/index.js and paste the following code

var reporting;
var payment;
var abTesting;
var clickTracking;
var experimentalFeature1;
var experimentalFeature2;

if (REPORTING) {
	reporting = require('./reporting.js');
}
if (PAYMENT) {
	payment = require('./payment.js');
}
if (A_B_TESTING) {
	abTesting = require('./abTesting.js');
}
if (CLICK_TRACING) {
	clickTracking = require('./clickTracking.js');
}
if (EXPERIMENTAL_FEATURE_1) {
	experimentalFeature1 = require('./experimental-1.js');
}
if (EXPERIMENTAL_FEATURE_2) {
	experimentalFeature2 = require('./experimental-2.js');
}

In the above code snippet, the variables like REPORTING, PAYMENT, CLICK_TRACKING would be avaiable to us in our bundle at compile time. Webpack will replace them with the values provide via DefinePlugin. Using these values, we are going to conditionally load our modules.

After adding dummy modules, lets build our bundle(s) and compare the results!

abTesting.js 		267 KB
clickTracking.js	107 KB
experimental-1.js	54 KB
experimental-2.js	54 KB
payment.js			214 KB
reporting.js		161 KB
  1. Config 1 — All modules enabled
{
	"REPORTING": true,
	"PAYMENT": true,
	"A_B_TESTING": true,
	"CLICK_TRACING": true,
	"EXPERIMENTAL_FEATURE_1": true,
	"EXPERIMENTAL_FEATURE_2": true
}

bundle.js			910 KB
  1. Config 2 — Reporting, Payment and Experimental Feature 1 enabled
{
	"REPORTING": true,
	"PAYMENT": true,
	"A_B_TESTING": false,
	"CLICK_TRACING": false,
	"EXPERIMENTAL_FEATURE_1": false,
	"EXPERIMENTAL_FEATURE_2": true
}

bundle.js 457 KB
Reduction in size - 49.78%
  1. Config 3 — A/B testing, Click Tracking, Experimental Feature 1 and Experimental Feature 2 enabled
{
	"REPORTING": false,
	"PAYMENT": false,
	"A_B_TESTING": true,
	"CLICK_TRACING": true,
	"EXPERIMENTAL_FEATURE_1": true,
	"EXPERIMENTAL_FEATURE_2": true
}

bundle.js 514 KB
Reduction in size - 43.52%

So, after comparing these results now we can see how DefinePlugin can help us in optimizing our JS bundle and provide more granular control over what goes into the bundle and what not.

You can find code for this blogpost here.

Hopefully, this article helped you in some way and if yes, then kindly tweet about it by clicking here Twitter Logo. Feel free to share your feedback here.


Yomesh Gupta

Hi, I am Yomesh Gupta. I am trying to find a perfect blend of design and technology! This is my blog where I write about things which fascinate me. Let me know your views here.

Newsletter.

Subscribe to get notified about new content. No spam ever!