I have been playing with Meteor 1.5 and it’s dynamic imports, which can be used to reduce the amount of javascript code sent to the browser.

This article is a deep dive into how to profile and optimize your client side code to make smarter use of the new dynamic imports…

Which will make meteor load faster for your users.

If you’re not already familiar with Meteor you should checkout the meteor website and the meteor chef Come back to this when you’re comfortable and want to optimize an existing application.

Meteor 1.5 landed, and it’s got a lot of improvements, but I think the big new feature that people want to know about is the ability to dynamically import code.

What is that?
Why is it useful?
How do I do it?
How do I know what to dynamically import?

Well, lets look into some of those questions, and how to use these new tools to improve your application’s load time.

This post is about identifying what to optimize and how to optimize… I will do another about how to develop/architect better dynamically imported components.

Setup a Basic Meteor Project

You can clone this repo and checkout the 01-initial branch. Or you can use the following instructions to get up to this starting point. Or you can jump right into this topic on your own project.

To do anything, we need a simple test project. I am starting with Meteor Chef Base and I am adding react-dates and a bit of functionality to pick date ranges (I also happen to know it has a noticeable impact on bundle size, which we will see later).

git clone https://github.com/themeteorchef/base.git meteor-example-optimize-client-bundle
cd meteor-example-optimize-client-bundle
rm -rf .git && git init && git add . && git add .meteor/*
meteor update --release 1.5
cat .meteor/release
  [email protected]
meteor npm install --save react-dates moment react-addons-shallow-compare
npm start

I then edited the code and added an example to create a simple example project.

screencast basic functionality

Not terrible for a quick example (and not a todo list).

Goal: Reduce Time to Initial Render

You can clone this repo and checkout the 02-before-optimization branch.

Make things faster, always. Initial render of the site/app is a great metric to improve on and one of the pain points for meteor apps.

When in production mode, Meteor builds all of your app into 1 big JS and 1 big CSS file - these are called the “client bundle”.

Meteor does it’s best to minimize those files, and cache them… but it can still be a whole lot of stuff sent to the client (this is all of your code and your code’s dependencies) on initial pageload… Especially if you don’t always need some of those components and dependencies.

To test and profile, we have to run in production mode.

meteor --production

Our super-tiny example app is not too bad, but could certainly get better.

Our biggest slowdown by far is the bundle of all javascript.

size download_time
JS bundle 494 KB 444 ms
CSS bundle 7 KB 29 ms

time-to-render-prod-mode-before-optimize

Wow, but this is such a tiny application… what can we do?

Plan: Profile to Determine Work to be Done

You should never optimize without profiling.

Sure, when you develop keep performance and optimization in mind - use best practices and all that. But when you are trying to figure out how you can improve performance, you should use tools to profile and get information about what to optimize. Without that profiling information, you will be treading water, spending lots of effort for little value.

How to Profile Meteor JavaScript Bundle?

So we know we need to reduce the total JavaScript Bundle… but how? What’s Big? What’s easy to remove? What’s not used?

Luckily, there is a now a bundle-visualizer package which gives us an awesome sundial graph for identifying size in our initial load.

meteor add bundle-visualizer
npm run start-as-prod
# ^ if you don't have my package.json scripts, you would run
#   meteor --production

UPDATE there is a new meteor 1.5.1+ feature where you don’t have to add the package you can just add it for the start… meteor --extra-packages bundle-visualizer --production which I have setup in package.json as npm run start-profiled

Then an overlay happens in your browser and you can hover over parts to see what they total size is for the part, and it’s children…

bundle-visualizer-before-optimize

This is AMAZING aw_yeah

Real quick review of some numbers here:

section percent
react-dom 9.4%
react-dates 9.1%
react-bootstrap 8.4%
bootstrap 6.4%
jquery 4.6%
handlebars 4.0%
app code 1.7%

app-1.7percent

My entire app code is 32k which is only 1.7% of the clientside bundle!

How can we optimize?

So we can look at our codebase in sections and do something…

What exactly will we even be doing?

It really comes down to 3 options:

  1. remove a section of code from our project entirely (best solution, but not often possible)
  2. reduce the size of a section (micro optimization, not usually effective)
  3. delay loading a section via dynamic import (our new option)

Remove

In this example, I could remove jquery or handlebars because I’m not using it… but Meteor Chef Base is…

This is outside of the scope of this article. If you can remove code, do so.

Reduce

There’s very little I could do to reduce the size of my code, my application code is super tiny.

This is usually the least valuable way to spend your time. Better to remove or delay a bunch first.

Delay

Ok… What can I delay, and what are the ramifications of doing so?

Basically you want to look for things which are only used once or twice, but which have a heavy footprint.

Pick high value targets - sections to delay

Armed with a profile of code sections (size), we try to identify a what we can remove from our initial bundle, and only load as needed.

We start by looking at the “biggest” sections of code (clockwise), and for each section we try to ask ourselves if we can remove or delay loading.

Pro Delay:

  • Is the section of code used only once or in a very few components?
  • Is the section of code an optional “secondary” feature?
  • Is the section of code a big enough percentage of the bundle to make it worthwhile?

Against Delay:

  • Is the component required to make the initial page render look ok to the user?
  • Is the page going to significantly jump when the component gets loaded dynamically, after the rest of the page has rendered?
  • Is the lack of that component going to mess things up for server side rendering?

In this case, react-dates is a great candidate, because I only use it once in my codebase, but it’s got a huge footprint in the clientside bundle.

Note: I’m using ag (the silver searcher) or rg (ripgrep) to search through file contents, lightning fast.

$ ag 'react-dates' imports -c
imports/ui/components/PickDates.js:1

Optimize: delay loading code with a dynamic import

Since we decided to target react-dates, I am going to dynamically load the only component which uses it, PickDates.

I could do this several ways, but here’s a very simple approach.

First I install a super-useful helper react-loadable, which handles the “maybe I’m loading, maybe I’m loaded” react component.

meteor npm install --save react-loadable

before: normal import part of clientside bundle

import PickDates from './PickDates';

after: dynamic import no longer a part of clientside bundle

import Loader from 'react-loadable';

// generic loading component to show while transfering section of code
const LoadingComponent = () => <span className="text-muted"><i className="fa fa-refresh" /></span>;
// new version of the component, now: loading --> rendered
const PickDates = Loader({
  // this does the dynamic import, and returns a promise
  loader: () => import('./PickDates'),
  // this is our generic loading display (optional)
  loading: LoadingComponent,
  // this is a delay before we decide to show our LoadingComponent (optional)
  delay: 200,
});

// there are more options you can use,
// including a timeout, and even a pre-loader... NEAT!

It can get a lot more complex, but this is a fine place to start.

  1. react-loadable is a HOC which will render our component when it’s available.
  2. The loader: <func> function recieves a promise which resolves as soon as the dynamic import is done.
  3. The import(<path>) is the where the actual dynamic import (magic) happens. It returns a promise which resolves when the component is on the client.
  4. The loading (Component) shows while the component is not yet loaded.
  5. The delay hides the LoadingComponent for this long, so there is less flicker in case the component is already cached and can just load-in super fast.

Because the import is no longer part of the initial clientside bundle, it is compiled with all of it’s dependencies, separately. They will be loaded later, the first time they are used, and then cached on the client. (more on this later)

Review: After our dynamic import

You can clone this repo and checkout the 04-after-optimization branch. NOTE (updated version on the master branch… code changed, this page was updated too… that old branch was not)

Did it do anything? Lets profile again.

npm run start-as-prod
# ^ if you don't have my package.json scripts, you would run
#   meteor --production
JS bundle size
before dynamic import 494 KB
after dynamic import 459 KB

after-dynamic-import-network-tab

And how about our sundial?

after-dynamic-import-bundle

It looks like we no longer have the react-dates module at all…

But if I get rid of the bundle-visualizer then I can see our date picker working…

meteor remove bundle-visualizer

UPDATE there is a new meteor 1.5.1+ feature where you don’t have to add the package, so thererefor you don’t have to remove it meteor --extra-packages bundle-visualizer --production

after-dynamic-import-still-working

Gotcha: You have to dynamic import ALL references

You could have MyComponent and OtherComponent, both of which included react-dates.

The react-dates section of code would only be removed from the initial clientside bundle if both MyComponent and OtherComponent were dynamically imported.

Any static import, ensures the component and all descendants are in the clientside bundle.

Understand: So how does it work?

Where is did the component go? How does it work?

If you use the Developer Tools > Network tab > WS (websocket) and look at the frames, you can find a request for the dynamic import, and the response with the dynamic import payload.

find-dynamic-imports-in-frames

request response

That’s actually kind of a big frame!

But it is cached, websocket transfer is pretty fast, and it only is loaded when it’s needed.

Furthermore, each component is only imported 1 time, even if imported via different paths.

Keep this in mind: Dynamic Imports are not Free

The dynamic import does make the section of code disappear from your clientside bundle.

It does “improve” the profiled metrics… but it has a few costs:

  • starting meteor is a wee bit slower (sorry devs)
  • added complexity
  • bundle over websocket can not be offloaded to a CDN
  • dynamic imports are not (currently) profileable… so to profile you’d have to make static imports again.

Built In Optimization: Each Component IS Only Imported 1 Time

You can clone this repo and checkout the 06-experiments branch.

I conducted a few experiments (more below) and I now better understand how the imports work.

Here is an obvious structure of components:

- MyPage
  - Trunk <-- dynamicly imported from a page
    - Limb
      - Branch <-- dynamicly imported from a page
        - Twig
          - Leaf

At first, my experiment was to log into the console, when JS “loaded” code.

ss

That caused me to believe they were in fact imported twice… but they were not.

In fact, if you look at the websocket frames, you can see that Leaf component is only imported 1 time. ss ss

And it is more obvious, when you load the Trunk first and the Branch second: ss

Then there is not second import for Branch, because it was already imported above…

This is REALLY impressive, and speaks highly of the module system in Meteor

Experiments: What can you do with dynamic imports?

You can clone this repo and checkout the 06-experiments branch.

I felt like trying out a few more variations on how things can be loaded.

This is a Work In Progress, more coming soon

  • import as in-file function, and in-file loader (works)
  • import as shared function, but in-file loader (works)
  • import as shared function, path as arg (fails, can not find the path)
  • import a npm package directly (fails)
  • import a component that imports a component (works, and a good idea, within reason)

Experiments: Profiling imports?

You can clone this repo and checkout the 07-profiling branch.

Summary

You can now segment your meteor clientside codebase.

  • You can profile and delay expensive components + dependencies
  • You can make an admin section (or whatever section) of your site only load when needed.
  • You can make every single route, only load when needed (sub-optimal, but effective, temporarily).
  • Or better, you can dynamically load individual components… (especially expensive ones)

Thanks & Credits

Huge thanks to Meteor, MDG, and especially ben… I’ve been learning a lot just by reading your commits, and I am grateful for your contributions.

Also a huge thanks to jesse for the bundle-visualizer and all your other meteor work.

Definitely read the Meteor Blog Post and maybe the huge pull request for information about dynamic imports.

I’ve also played with code from meteor’s react-loadable example which helped me to understand dynamic imports.

Other good reading, webpack’s dynamic import docs are useful too.