Putting Your App on a Diet with Meteor 1.5’s Bundle Visualizer

Taking Vulcan from 4.2MB to 1.98MB

Sacha Greif
Meteor Blog

--

Meteor 1.5 just came out, and the big, shiny, new feature is dynamic imports.

But a really cool tool also shipped alongside that feature: the Bundle Visualizer:

This shows you on a graph exactly which Meteor and NPM packages are taking up the most space in your bundle.

To use it, just upgrade your app to Meteor 1.5, add the bundle-visualizer package to it, and then run your app in production mode (meteor --production).

The visualizer may reveal some surprising facts, as it did when I used it on Vulcan!

First Pass: 4.2MB!

I had put off focusing on bundle size for far too long, and it showed! The bundle clocked in at a massive 4.2MB, without gzip:

If you look at the orange bar (second from the center) you can see there’s one long, continuous segment, and then it breaks down in smaller chunks. That continuous segment corresponds to the app’s NPM packages (in other words, the dreaded node_modules folder), which drastically outweigh the actual Meteor code (the little chunks).

Here’s a breakdown of the biggest culprits:

  • intl: 935kb
  • react-intl: 341kb
  • intl-relativeformat: 331kb
  • react-dom: 181kb
  • graphql: 171kb
  • react-bootstrap: 161kb
  • handlebars: 75.8kb
  • core-js: 75.2kb
  • lodash: 72.8kb
  • elliptic: 72.5kb
  • apollo-client: 64kb
  • crypto-js: 56kb
  • intl-messageformat: 55.1kb
  • moment: 49kb
  • simple-schema: 39kb

As you can see, the largest chunk by far was the internationalization packages, with 1.6MB altogether! In other words, 40% of the bundle size was taken up by a feature a majority of people might not always need.

intl, react-intl, and intl-relativeformat: almost 40% of my bundle combined!

Getting Rid of Internationalization (But Not Really)

Now I didn’t want to just throw out internationalization altogether. And I also didn’t want to force people to refactor all their code to get rid of calls to react-intl’s APIs.

So I found a good middle ground: I created a new simplifiedvulcan:i18n package that uses the exact same APIs as react-intl, but only includes its most common features (and none of its dependencies!).

Just replace any react-intl imports by vulcan:i18n imports and you’re good to go! And if you do need the full power of react-intl, creating your own copy of vulcan:i18n that re-exports react-intl shouldn’t be too hard:

export * from 'react-intl`;

Obviously this is not applicable to every situation, but creating your own package versions that only include the features you need can sometimes be a great way to slim down your app.

Result: 1.6MB saved!

Fixing My Imports

Going down the list (or rather, around the circle) another big source of dead weight was react-bootstrap.

Before: importing everything

As useful as that library is, I was only using a tiny fraction of the components it provides. I thought about getting rid of it altogether, but that would mean having to code my own drop-downs, modals, etc. Not fun!

Instead, I found a much simpler fix. It turns out that there is a big difference between:

import { Foo } from 'react-bootstrap';

And

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

The first one will import the entire contents of the package, while the second one will only import the Foo component and its dependencies!

I converted all my imports to the second syntax, and the results speak for themselves:

After: importing only what’s needed

Finding Dependencies

One thing the visualizer doesn’t tell you is where a dependency comes from. For example, I discovered that handlebars was somehow being bundled on the client, but couldn’t tell which NPM package was requiring it.

Turns out there’s a simple command that lets you know just that:

npm ls handlebars

The result told me that handlebars was a dependency of simple-schema, and since I’m pretty sure it’s not really needed, I’m looking forward to shaving off a couple more KBs from my bundle size once that gets removed:

├── handlebars@4.0.6
└─┬ simpl-schema@0.2.3
└─┬ message-box@0.0.2
└── handlebars@4.0.6 dedu

Post-Diet Weight: 1.98MB

After all these optimizations and more, I managed to get the bundle size down to 1.98MB, in other words less than 50% of what I started with!

The funny part: I haven’t even had to use dynamic imports to get these results. I’m looking forward to implementing them and getting even better gains (or losses, I guess?), but I’ll leave that for another post.

--

--

Designer/developer from Paris, now living in Osaka. Creator of Sidebar, VulcanJS, and co-author of Discover Meteor.