Reducing JavaScript Bundle Size. Part Two: Minification and dead code…
By: Denny Scott
Tags: Webpack, Ecmascript 6, React, JavaScript, Performance

Automating with Webpack!

At one point, we did have to pass our code through tools like Ternary before releasing our app to the web. But since then, bundling tools like Rollup and Webpack have taken over the front end community. These bundling tools are usually straightforward to extend with plugins like Terser. In our example, we are going to be using Webpack and adding the Terser plugin.

Note: There are other web application bundlers, so don’t think this is an exhausting list. These are just the main ones used today. Parcel is newer, but very exciting!

So let’s add Webpack and the Webpack-CLI to our project:

npm i webpack webpack-cli — save-dev

We’ll be adding and moving a couple of files here. First, we are going to add a folder called dist and src :

mkdir dist

mkdir src

Inside of dist , we will add the index.html file:



<!doctype html>

<html>

<head>

<title>Getting Started</title>

</head>

<body>

<script src=”main.js”></script>

</body>

</html>

Then we are going to navigate to our /src folder at the root of our project, and move index.js into it:

mv index.js src/

Lastly, we need to go into our package.json , and remove the main property, and instead add private: true :

Our project is set up, so we’ll return to the root of the project, and run the webpack-CLI tool.

npx webpack

After that, we can open up /dist/main.js to see our new bundled JavaScript file. Remember, this is adding a bundling system to our app, so there’s going to be quite a bit of boilerplate attached to the file. This is a significant amount of overkill for only having a single JavaScript file. But as our web app gets more abundant, this boilerplate becomes an insignificant amount of code.

So let’s look at that bundle!

!function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){“undefined”!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:”Module”}),Object.defineProperty(e,”__esModule”,{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&”object”==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,”default”,{enumerable:!0,value:e}),2&t&&”string”!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,”a”,t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p=””,r(r.s=0)}([function(e,t){}]);

You may have a couple of questions. First off, we had talked about adding the Terser plugin, but this is already minified and compressed. Well, that’s because Webpack automatically adds the Terser plugin for minification. Also, you may have noticed a warning in your console when we ran Webpack. Since we are not specifying an environment, it assumed production. When the environment is set to production, it will perform the minifications and compression for us.

Maybe an even more critical question, where is the code we wrote? Well, the most recent version of Webpack wraps your file with esmodules , so it can do a bit more static analysis on your module. It determined that the code we wrote had no effects on our code base. The function was never called externally (more on that in part three) and never executed any non-pure paths. Therefore, the entire file was dead-code. And that’s correct! We never used that code with the actual front-end. The easiest way for us to get around this is to add a console log. So let’s do that in our index.js .

Now when we run our webpack CLI again:

!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){“undefined”!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:”Module”}),Object.defineProperty(e,”__esModule”,{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&”object”==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,”default”,{enumerable:!0,value:e}),2&t&&”string”!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,”a”,t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p=””,n(n.s=0)}([function(e,t){const n=”shirt”===”shirt”?{type:”shirt”,amount:”$10"}:{type:”pants”,amount:”$12"};console.log(n)}]);

Our code now exists in the main bundle. Also note, just like before, our desired dead code was eliminated! Let’s try abstracting this a bit more. In our index.js , let’s update our code to:

And now the result:

!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){“undefined”!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:”Module”}),Object.defineProperty(e,”__esModule”,{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&”object”==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,”default”,{enumerable:!0,value:e}),2&t&&”string”!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,”a”,t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p=””,n(n.s=0)}([function(e,t){const n=()=>({type:”pants”,amount:”$12"});const r=”shirt”===”shirt”?(()=>({type:”shirt”,amount:”$10"}))():n();console.log(r)}]);

Notice that not only is the call to buyHat removed, but the actual function has been removed as well. Webpack noticed that the request was not used anywhere and removed the unreachable code. If you were to run the same module through only Terser, and not using Webpack, you would get the following output:

const buyShirt=()=>({type:”shirt”,amount:”$10"}),buyPants=()=>({type:”pants”,amount:”$12"}),buyHat=()=>({type:”hat”,amount:”$5"});function selectClothes(type){return”shirt”===type?buyShirt():buyPants()}const clothes=selectClothes(“shirt”);console.log(clothes);

So Webpack has removed the unused function, while Terser is unable to do this, why is that?

This is because Webpack can assume we are using the esmodule system. As of Webpack 4, this is the default module system used. Since the buyHat function is not exported (more on this later), Webpack knows the only place this is used in the code is in our selectClothes function. Once Terser removes the dead code from there, Webpack knows it’s no longer used anywhere in the codebase, and can safely be removed. This is an early demo of Treeshaking.

Read more on treeshaking in my follow up article here