Meteor 1.5 ~ Bundle Optimization + Examples
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.
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 |
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 inpackage.json
asnpm 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…
This is AMAZING
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% |
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:
- remove a section of code from our project entirely (best solution, but not often possible)
- reduce the size of a section (micro optimization, not usually effective)
- 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.
- react-loadable is a HOC which will render our component when it’s available.
- The
loader: <func>
function recieves a promise which resolves as soon as the dynamic import is done. - 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. - The
loading
(Component) shows while the component is not yet loaded. - The
delay
hides theLoadingComponent
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 themaster
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 |
And how about our sundial?
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
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.
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.
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.
And it is more obvious, when you load the Trunk first and the Branch second:
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.
- import (static)
lodash.cloneDeep
=8.9kb
screenshot - import (static)
lodash/cloneDeep
=19.5kb
screenshot screenshot2
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.