Dynamic imports in Meteor 1.5

A preview of the next big development in the platform

Sashko Stubailo
Meteor Blog

--

The Meteor 1.4.x series of releases was focused on improving the developer experience of large Meteor apps and making it easier to ship incremental improvements to core packages. It brought dramatically improved rebuild times, upgraded versions of dependencies like Node, and improved decoupling of core components. We’ve also put a whole ton of work into making it easier to participate in the Meteor project than ever before, with a whole host of guides and processes for interested developers to submit bugs, fixes, and features to the project. We’ve seen these developments reflected in positive feedback from teams using Meteor in production as well as contributors on GitHub.

The Meteor 1.5 series is going to be focused on the next big request we’re hearing about from developers: Production performance, especially around application load and startup time. People expect the internet to be fast, and there are now many techniques to make even complex web applications load almost instantly. The primary technique has been called “code splitting”, and something similar has recently been moving through the ECMAScript standards process as a feature called dynamic import.

Watch the demo

To get a sense of what using dynamic imports is like in the current Meteor beta, watch Ben Newman’s demo video presented at a recent Meteor Night:

A dynamic import is an import statement that returns a promise instead of synchronously returning the module:

// Static import
import MyComponent from './component';
render(MyComponent);
// Dynamic import
import('./component').then((MyComponent) => {
render(MyComponent);
});

This gives the module system an opportunity to do something that takes time, like loading that module from the server asynchronously. By using dynamic imports in the right places, you can dramatically reduce the bundle size and startup time of your app!

Just by updating to Meteor 1.5 you can start optimizing your page startup time right away by changing some imports to be dynamic, without writing any configuration.

You can try this functionality today by running the current beta release of 1.5! Find the newest beta by visiting the PR on GitHub.

By changing a little bit about how you do imports, you can get two types of benefits:

  1. Bundle size. Dynamic modules don’t have to be loaded at all when your application starts up, which means your users don’t have to download as many bytes of code to see the initial page render.
  2. Execution time. Even without dynamic imports, moving some of your static import statements deeper in your app, can speed up the initial startup time of your app by running less code. For example, you might want to put the import statement inside a router callback or various conditional statements. You still pay the network cost of loading those modules but your application initializes faster than before. Since technically ECMAScript 2015 imports are only supported at the top level or a module, this is a Meteor-specific feature, which is usually achieved in other frameworks by typing require directly.

What’s special about Meteor’s dynamic imports?

There have been several implementations of code splitting and dynamic imports in the JavaScript community, with some particularly notable ones being Webpack, System.js, and Next.js. All of these systems are great, and every module bundler will come with some set of tradeoffs. Here are the benefits of the approach taken in Meteor:

Flexibility without configuration. Like you might expect from Meteor, you have to write exactly zero configuration to start using dynamic imports in Meteor 1.5. Simply write const React = await import('react') instead of import React from 'react' whenever you want a module to load dynamically instead of being included in the startup bundle. At the same time, there’s no special magic — you can control exactly which modules are dynamic without that being coupled to a particular router or UI framework. It works just as well with React or Angular 2 or Moment.js.

Exact code splitting. Some code splitting approaches, for example the one in Webpack, work by generating a specified set of code “bundles” — these are each a collection of modules your app expects to use together. However, sometimes those bundles will have significant overlapping parts, meaning you have to load the same page, component, or library multiple times during normal app operation. One suggestion these systems make is to put common dependencies into a “vendor” bundle that is shared between the other code. In Meteor’s system, there are no defined entry points or compiled bundles — instead, the server gives the client exactly the set of bundles it needs and no more. We call this “exact code splitting” and it means you load every bundle exactly once.

Works side-by-side with existing Meteor code. No Meteor solution would be complete without a backwards compatibility and migration story. You can start using dynamic imports in your app right away even if the rest of your code is written using “classic” Meteor package-scoped variables. Just pick a particularly expensive module or npm package, request it with a dynamic import(...), and you’re off to the races! You can even split in the middle of a package, so people can ship Meteor and npm packages that don’t load all of the code at once.

Works the same way across client and server. Most bundling systems are only designed to work with client-side code, but many Meteor applications are written in a way where modules are often shared between client and server. If you load a module with dynamic imports in your server code, it will work just fine, since on the server these dynamic imports will compile to a simple Node.js require.

Spec compliant and debuggable. We’ve gone out of our way to make sure that transpiled modules in Meteor are spec-compliant, with important features like live bindings. One more great thing is that the transpiled code is easy to debug on the client. No weird variable names like _require_0, just the module name you expected.

Look in the bottom right — the local variables are exactly what you would expect.

How it works

So what does Meteor do differently that enables some of these features? Well, here’s a summary:

The most important aspect is that the client is aware of the shape of the module tree, the names of those modules, and their content hashes ahead of time for the entire app. This means that when a dynamic import happens, the client-side app can determine exactly which modules and hashes are missing, and ask only for those from the server in one request. Then, all of the modules are cached individually on the client so that next time they can be loaded from persistent storage.

This means that, in most cases, you can load every dynamic module exactly once. Even if you push an entirely new version of your app, if that’s using the same version of a library as before that will still be loaded from local storage.

Use case

So what should you be importing dynamically? Where should you first wield this awesome power of being able load new code at will?

Well, it’s all about not loading code you’re not planning on using. Anything that isn’t important for the initial page load should be imported dynamically:

  1. Rarely-used routes, like an admin panel. You might even want to dynamically load all of your routes.
  2. Expensive UI components, like a PDF viewer, embedded map, or complex data visualization.
  3. Big libraries, like moment.js, which are only used for small details on the page.
  4. Language packs for internationalization should only be loaded once a user selects that language.

It’s still early days for Meteor 1.5, but before the final release we hope to enable tooling that helps you understand which parts of your app should be split off, so that you don’t have to think too hard about these decisions.

Join the conversation!

Whether you’re a current Meteor developer looking to make your app more performant, or a JavaScript guru looking for a simpler way to get the best features of modern JavaScript, you’ll want to get involved in the discussion about Meteor 1.5 and dynamic imports.

Get involved and give feedback on the GitHub PR for release 1.5. Let’s build something great together!

--

--

engineer at @stripe. previously open source eng manager at @apollographql and @meteorjs, https://github.com/stubailo