DEV Community

Eryk Napierała
Eryk Napierała

Posted on

Why does your bundle grow?

Growth is an inherent property of development

One may say. Every time you're adding some feature to your application, the size of the codebase and the resulting bundle increases (the opposite thing is what they call "refactoring"). It's nothing wrong with growth until you keep track of these changes and you know exactly why files shipped to the browser are bigger than before. The task is quite difficult, as web applications nowadays are incredibly complex - it's no longer just your code, most of it (in terms of size) are external libraries. The build system matters as well - all the transpilers and bundlers completely changes how the app code looks for the end-user. When any of those parts changes, the bundle may change and very often it's unpredictable and unexpected. Bumping some small package by the minor version or changing a single option in the tooling configuration is enough to get extra kilobytes that you may not even need. How do you know if anything unnecessary was added to your bundle, and what exactly was it?

Understanding is the key

There are many great tools for visualization of the bundle structure, like Webpack's Analyse and webpack-bundle-analzer. They help to understand what the bundle consists of but still don't solve the problem of tracking changes between subsequent builds. Unless you're having fun with "spot the difference" game.

Two webpack-bundle-analyzer reports placed next to each other for comparison

For the source code git diff is a perfect tool, but you what about built artifacts? Keeping them in the repository to manually review compiled code doesn't sound really exciting. There is a tool that may help with this miserable situation: webpack-stats-explorer.

webpack-stats-explorer overview

Example

The common project setup these days includes Babel, which transpiles usually succinct ES6+ code to quite verbose ES5 counterpart. Let's look how relatively small changes to source code may affect the final bundle significantly.

Consider a very simple module, literally 57 characters long. It's actually pure ES5 code, so the output is perfectly predictable.

export default function (a, b, c) {
  return [a, b, c];
}

What if we'd add some ES6 feature? A simple one, like a default value for the function argument. It's just four characters including spaces!

export default function (a, b = 1, c) {
  return [a, b, c];
}

The ES5 output will be significantly bigger. And I mean it - 137 characters.

function _default(a) {
  var b = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : b;
  var c = arguments.length > 2 ? arguments[2] : undefined;
  return [a, b, c];
}

Of course, before shipping to production, this code will be minified, but it'd be still 92 characters - 23 times bigger than you may expect. That's how it's displayed in webpack-stats-explorer.

The difference made by adding default function argument visualized in webpack-stats-explorer

You may say, that 100 bytes don't make a difference. Multiply it a few hundred times and you will get tens of kilobytes.

But there are more creatures that may scare you. Let's look at this little snippet - asynchronous function.

export default async function (a, b = 1, c) {
  return [a, b, await Promise.all(c)];
}

webpack-stats-explorer shows increase of 1,42 kB. It's just 25 characters in the source! That's huge.

The difference made by making the function async visualized in webpack-stats-explorer

And you know what? This code doesn't even work. You need to add the whole regenerator runtime library, which costs the next 20 kilobytes. So it's 22 000 bytes for 25. Nice ratio, isn't it?

The difference made by adding regenerator runtime library visualized in webpack-stats-explorer

In case it wasn't convincing enough, think of a completely different situation: upgrading dependencies. It's quite an easy and straightforward task when it's a patch or minor bump and the library you use follows semver convention. So you just launch ncu -u and npm install, you run unit tests and if they pass, git commit -m "Upgrade dependencies" && git push. Voilà, the maintenance part is done, let's go back to doing fun things.

But sometimes things go worse. If you used react-apollo for a while, you may remember this issue. With a little patch, you could get almost 10 kB of code (after minification) just for free. The issue was fixed after a while, so if you knew, you could, well, react. Like wait or help to resolve it.

webpack-stats-explorer showing the difference after upgrading dependency by the patch version

Those are only two simple examples, but the problem surface is much greater. Think about all these times your bundle exceeded the limit and you've just increased it with heavy hearth sighing: "sure, I've added some code and bumped dependencies, development has a cost". Are you sure you had not missed anything obvious?

Conclusion

You should be aware of what is going on in your code - not only the source but built one as well. With that knowledge, you can decide, if you really need all these extra pieces you've got for different reasons. If users downloading all this JavaScript with every page reload need it. You at least have a chance to do something about it: tweak build system configuration, skip upgrading a library or stick to .then this time.

If you have Webpack in your project, give webpack-stats-explorer a chance. Next time you are going to merge a feature branch to master, review not only the source code but production bundle as well.

Appendix

webpack-stats-explorer is an open-source, non-profit side project created with React and ReasonML. It may be a good opportunity to try bleeding-edge technology in a real-world scenario. There is a backlog full of ideas, but also with space for bug reports and suggestions. Any help will be appreciated!

Top comments (1)

Collapse
 
pavelloz profile image
Paweł Kowalski

So... in short: dependencies. Libraries, polyfills, babel plugins. Less is more.