small bundle size
Improve the performance of your Meteor app

Aug 2018

Meteor bundles up your code, and all the external JavaScript libraries that you rely on, into a single JavaScript file. As your app grows in complexity and dependency count, the bundle size will increase. It's easy for this to quickly get out of control.

In this post I will show you how to significantly reduce your bundle size by using dynamic imports and tree shaking, within your Meteor app.

You'll see how I slashed my bundle from 966kB to 399kB 📉
Gzipped, that's 259kB down to 123kB. 🚀

1. Base app

To illustrate these techniques, I created a simple Meteor app which consists of two screens: thehomescreen and thetestscreen .

Thehome screen (App.js) is a lightweight component; the only bit of heavy-lifting it does is declare routes, usingreact-router. Thetestscreen (Test.js) imports three extra libraries:material-ui,moment, andlodash.

Running the bundle visualiser, we see that the bundle size of this app totals 966kB. Particular modules to takes note of are:

  • Material UI - 347kB
  • Lodash - 72.1kB
  • Moment - 53.1kB
Meteor bundle size visualisation

The Google lighthouse audit rates the performance of our simple app 89/100. This is a decent rating but, Lighthouse does have a gripe with the 651ms script evaluation time of the meteor.js bundle. We'll improve this and also reduce the bundle size in the following steps.

2. Tree shaking

This step almost halves the bundle size (to 536kB) and it takes minimal effort to implement ✌️

While we can't tree shake our entire app yet, we can eliminate unused modules from bothmaterial-uiandlodash. This is accomplished with the babel-plugin-lodash and babel-plugin-direct-import packages. Install using the following command:

meteor npm i --save-dev babel-plugin-lodash babel-plugin-direct-import@^0.6.0-beta.1

Then, in the root of your app, create the following.babelrc file:

Now, when our app compiles, the unused modules from material-ui and lodash disappear.

Looking at the bundle viz we can see that the app is now only 536kB. We can see why when we look at the size of the following imports:

  • Material UI - 32.2kB (was 347kB)
  • Lodash - 21.7kB (was 72.1kB)
  • Moment - 53.1kB (unchanged)
Meteor bundle size visualisation

Meteor now only bundles thematerial-ui and lodash code that we actually use.

The Google Lighthouse rating is now 95/100 and it no longer complains about script evaluation time. #winning

3. Dynamic imports with react-router

The app's home page is very simple. It doesn't requirematerial-ui ,lodashormoment. Yet, the client still has to download those modules when they visit the homepage. What if we could defer those downloads until the client actually needed them?

Dynamic Imports allow us to do just that.

While a statically imported module would be bundled into the initial JavaScript bundle, a dynamically imported module is fetched from the server at runtime.
Once a module is fetched dynamically from the server, it is cached permanently on the client and additional requests for the same version of the module will not incur the round-trip request to the server. If the module is changed then a fresh copy will always be retrieved from the server.

There are a few ways that we can approach this:

  1. Import extra modules based on what the user is likely to need.  In essence, admin users (who have way more screens and use extra libraries - charts, anyone?) would dynamically import more routes and hence more components + modules than unauthenticated users.
  2. Dynamically import modules when you use them. This will work fine, but doing this on a module-by-module basis leads to spaghetti code.
  3. Dynamically import individual components. To get the initial bundle size  really small, dynamically import on a route-by-route or component-by-component basis.

In a previous blog post I showed you how to dynamically import groups of routes (method 1).

Method 2 is a bit rudimentary. It's far easier to dynamically import a single component, than dynamically import a bunch of modules within a component.

Method 3 is my preferred solution and I've created a higher order component to do just this. Use it at thereact-router level (i.e. pass this HOC to a route, so that a screen is dynamically loaded) , or on its own when you want to defer the loading of a child component. Situations where this HOC is useful:

  • When you've got a single react component that pulls in a beefy module - e.g. a chart component that importsd3.js
  • When you want to keep the load time of your home screen as fast as possible. You can dynamically load your other screens when the user requests them.

How does it work?

DefferedComponent extends React.Component. When the component mounts, it goes and does the dynamic import. Once the import has finished it will re-render, using the newly imported component. The code is shown below, or you can install the@ninjapixel/meteor-defered-component module by typing the following into your terminal:

meteor npm i --save @ninjapixel/meteor-deferred-component

How to use it

In the following examples, I dynamically load a component and a screen (usingreact-router v4). We need to specify the path of the react component to be loaded; unfortunately though, we can't just pass this path toDeferredComponent becasue dynamic imports need be be declared upfront, with static strings, so that the compiler can do it's thang. We work around this by passing a function (importFunction) that returns the dynamic import.

Using this technique to import the screen, the bundle has dropped to 399kB. Check out the size of the following imports:

  • Material UI - 4.44kB (was 347kB)
  • Lodash - 0kB (was 72.1kB)
  • Moment - 0kB (was 53.1kB)
Meteor bundle size visualisation, of a tiny bundle.Fast Google Lighthouse score for Meteor.js app

4. Blaze and jQuery

Many UI packages in the Meteor ecosystem depend on Blaze (38.7kB). In turn, Blaze depends on jQuery (96.5kB). So, if you're not using Blaze for your front-end, make sure you're not using packages that depend on Blaze (e.g the accounts-ui package).

5. Summary

There are massive performance gains to be had from only importing the code you need into your initial client bundle. It's pretty easy to implement, too. I'd love to know what you use this for, so hit me up in the comments with what you're up to!

Please enable JavaScript to view the comments powered by Disqus.