How Did I Not Know This! — Webpack Bundling

Ever wonder why your bundle size is so large only to have used a few dependencies?

I created a simple project with a few production dependencies — React, ReactDOM and a Button component from react-bootstrap.

import React from 'react';
import ReactDOM from 'react-dom';
import { Button } from 'react-bootstrap';

Following, I ran webpack in production and was shock about the bundle size even in production! 359kb is quite a bit to send out especially for first time users that could potentially be on very slow internet connections. We can do some code splitting and caching to reuse resources but still, this is just a bare, bare minimal application that shouldn’t be this large — All I did was hook a ‘Hello World’ application to the DOM with a Button from react-bootstrap.

Version: webpack 2.2.1
Time: 20143ms
Asset Size Chunks Chunk Names
bundle.js 359 kB 0 [emitted] [big] main
[0] ./~/react/react.js 55 bytes {0} [built]
[17] ./~/react-dom/index.js 58 bytes {0} [built]
[24] ./~/react-bootstrap/es/SafeAnchor.js 3.75 kB {0} [built]
[25] ./~/react-dom/lib/ReactUpdates.js 9.67 kB {0} [built]
[44] ./~/react-dom/lib/ReactReconciler.js 6.29 kB {0} [built]
[45] ./~/react/lib/React.js 2.71 kB {0} [built]
[194] ./src/App.js 64 bytes {0} [built]
[314] ./~/react-bootstrap/es/Pager.js 2.93 kB {0} [built]
[331] ./~/react-bootstrap/es/index.js 10.8 kB {0} [built]
[349] ./~/react-dom/lib/ReactDOM.js 5.16 kB {0} [built]
[375] ./~/react-dom/lib/ReactVersion.js 350 bytes {0} [built]
[391] ./~/react-dom/lib/findDOMNode.js 2.46 kB {0} [built]
[415] ./~/react/lib/ReactVersion.js 350 bytes {0} [built]
[416] ./~/react/lib/onlyChild.js 1.34 kB {0} [built]
[420] ./src/index.js 781 bytes {0} [built]
+ 406 hidden modules

There is something wrong for sure. So, I began by seeing the file size for just requiring two dependencies — React and ReactDOM.

Version: webpack 2.2.1
Time: 8397ms
Asset Size Chunks Chunk Names
bundle.js 146 kB 0 [emitted] main
[3] ./~/object-assign/index.js 2.1 kB {0} [built]
[15] ./~/react/lib/React.js 2.71 kB {0} [built]
[16] ./~/react/lib/ReactElement.js 11.6 kB {0} [built]
[76] ./~/react-dom/index.js 58 bytes {0} [built]
[77] ./~/react/react.js 55 bytes {0} [built]
[78] ./src/App.js 65 bytes {0} [built]
[104] ./~/react-dom/lib/ReactDOM.js 5.16 kB {0} [built]
[146] ./~/react-dom/lib/findDOMNode.js 2.46 kB {0} [built]
[154] ./~/react-dom/lib/renderSubtreeIntoContainer.js 422 bytes {0} [built]
[158] ./~/react/lib/ReactClass.js 27.1 kB {0} [built]
[159] ./~/react/lib/ReactDOMFactories.js 5.53 kB {0} [built]
[160] ./~/react/lib/ReactPropTypes.js 16.2 kB {0} [built]
[162] ./~/react/lib/ReactPureComponent.js 1.32 kB {0} [built]
[163] ./~/react/lib/ReactVersion.js 350 bytes {0} [built]
[166] ./src/index.js 781 bytes {0} [built]
+ 152 hidden modules

Okay about ~146kb. Sounds about right according to React’s specs. So that means importing that Button component from react-bootstrap took a whooping ~213kb! Whoa, whoa wait a minute. That clearly doesn’t make sense. All I did was imported a Button from react-bootstrap. I’m not requiring the whole library now… or am I?

Let’s look at the imports again.

import React from 'react';
import ReactDOM from 'react-dom';
import { Button } from 'react-bootstrap';

Ah-ha, it hit me. I see what I did now. What is happening ‘under the hood’?

import { Button } from 'react-bootstrap';

is the equivalent to

const { Button } = require('react-bootstrap');

which is equivalent to

const Button = {/*react-bootstrap module*/}.Button;

So, ultimately, what’s happening is that I did import the whole react-bootstrap library. I had to before being able to get to the Button!

So, what can we do to just get exactly what we want? Get it directly!

import Button from 'react-bootstrap/lib/Button';

And running webpack in production again produces

Version: webpack 2.2.1
Time: 9681ms
Asset Size Chunks Chunk Names
bundle.js 174 kB 0 [emitted] main
[8] ./~/react-dom/lib/ReactUpdates.js 9.67 kB {0} [built]
[25] ./~/react/lib/React.js 2.71 kB {0} [built]
[28] ./~/react/react.js 55 bytes {0} [built]
[127] ./~/react-dom/index.js 58 bytes {0} [built]
[128] ./src/App.js 213 bytes {0} [built]
[187] ./~/react-bootstrap/lib/Button.js 4.47 kB {0} [built]
[203] ./~/react-dom/lib/ReactDOM.js 5.16 kB {0} [built]
[229] ./~/react-dom/lib/ReactVersion.js 350 bytes {0} [built]
[245] ./~/react-dom/lib/findDOMNode.js 2.46 kB {0} [built]
[253] ./~/react-dom/lib/renderSubtreeIntoContainer.js 422 bytes {0} [built]
[257] ./~/react/lib/ReactChildren.js 6.19 kB {0} [built]
[258] ./~/react/lib/ReactClass.js 27.1 kB {0} [built]
[259] ./~/react/lib/ReactDOMFactories.js 5.53 kB {0} [built]
[260] ./~/react/lib/ReactPropTypes.js 16.2 kB {0} [built]
[266] ./src/index.js 781 bytes {0} [built]
+ 252 hidden modules

a ~28kb difference now. That is much better. But that is still quite a bit for a Button component =)

Conclusion

The lesson is always check your bundle size and do a baseline comparison in every new build and think critically about your dependencies. Clients always come first. Make them happy and you’ll be happy. Also, don’t depend on destructuring when requiring dependencies to lower your build size. It is just purely for being more explicit about your requires.