Javascript build size optimization
Back at the end of 2017, the Hanzi writer library had a bundle size of 185 Kb - minified. I didn’t understand why this was, since there wasn’t that much code in the library itself, certainly not 185 Kb worth. The library included SVG.js, and was built with Webpack and Babel, but that was about it. Besides that it was a few thousand lines of code at most. I began digging into the bundle to try to figure out where the bloat was coming from, and kept finding more and more unnecessary sources of bloat, some obvious and some more hidden. In the end, I was able to reduce the minified file size by 88% down to 22 Kb! I’ll go into detail about all the steps taken to reach this level of optimization below.
Webpack bundle analyzer
Webpack has a built-in profiler that can output stats about where the chunks in your bundle come from. It gives stats before minification, but it’s still useful to get a rough idea of where the biggest wins are in terms of external dependencies and libraries. You can run the profiler by adding --profile
option to webpack. There’s some nice third-party tools that can analyze the output of the profiler as well. You can find more info about using the profiler here.
In the case of Hanzi writer, the biggest culprits were SVG.js, which I expected, but even bigger was babel-polyfill. I added babel-polyfill thinking it would be a nice way to make Hanzi writer backwards compatible, but it’s not strictly necessary, and if users of the library need backwards compatibility they can polyfill old methods themselves. Removing babel-polyfill took 1 line of code and dropped the minified file size from 185 Kb to 97 Kb! Babel-polyfill was literally 50% of the bundle size of the library!
Using raw JS for SVG animation
SVG.js was the next biggest culprit for the file bloat, but removing it wasn’t as easy since I needed a way to manipulate and animate SVG in JS still. Fortunately, it turns out that for most SVG manipulation and animation there’s no need for a third-party library at all. It’s easy to set SVG properties using raw browser APIs like Element.setAttributeNS, and animation is a breeze using window.requestAnimationFrame. I wrote a more detailed post on animating SVG using raw JS if you’re curious for more details on how this is done.
Using raw JS and browser APIs for SVG animation made it possible to remove SVG.js entirely, dropping the file size from 97 Kb minified to around 45 Kb - not bad!
Removing bloated Babel transforms
At this point, there were no more external libraries to remove. However, opening up the files generated by Webpack in a text editor revealed that there was still a lot of external code, and worse, it was being duplicated over and over again. The problem is that Webpack first runs each file through Babel, and then concatenates the generated files together into the final bundle. This would be fine, except that certain Babel transforms work by pasting helper code into the top of every file. For example, Babel copies the following lines of helper code into the top of every file that uses an es6 class:
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
This code is pretty dense, and was repeated again and again throughout the generated bundle from Webpack. I found that the es6 features that were generating the most bloat that were used in Hanzi writer were classes, for/of loops, and es6 import statements. It was sad to say goodbye to those nice es6 features, but rewriting Hanzi Writer using oldschool es5 prototypes, requires, and loops was another big win. The final minified file size dropped down to around 28 Kb minified!
It’s worth taking a look through the code generated by Webpack to get a sense if there’s anything dubious going on like duplicated helper functions from Babel. It might be possible to remove this duplication using the babel-minifiy-webpack-plugin or something similar. This duplicated helper code can take up a large portion of the generated bundle!
Aggressive minification settings
A benefit of having no external dependencies is that you know for sure how your code is structured. As a result, it’s possible to enable some more aggressive minification settings in Uglify. Uglify has a setting to mangle property names of objects, which can have a big impact on reducing file size. However, this is dangerous because code frequently accesses object keys by string, which will break if Uglify mangles them. Also, this will likely break the public API of the library you’re building since all the method names of classes and objects will be mangled!
Hanzi writer follows a convention that private methods of classes start with an underscore, ex this._doSomethingPrivate()
. Since private methods are never called externally, it’s safe for Uglify to mangle these method names during minification. You can pass a regex to uglify options to mangle only properties that match a certain pattern; in our case the regex is /^_/
. Turning on the extra minification dropped the file size down to the final 22 Kb!
The bundle size for Hanzi writer has slowly crept back up to 30 Kb over the past year as more features have been added, but it’s still a dramatic improvement over the original 185 Kb monster. Hopefully these techniques can help you optimize your bundle sizes as well. If there’s other techniques that I’ve missed for optimizing file size even further leave a comment below!