Implementation

Lazy-loading is easily accomplishable by applying dynamic import. Dynamic import is currently a Stage 3 ECMA feature. It works similarly to static import, but makes the following possible[1]:

import a module on-demand (or conditionally)

compute the module specifier at runtime

import a module from within a regular script (as opposed to a module).

Ember community doesn’t sleep though — the feature’s adoption is already available thanks to a brilliant addon called ember-auto-import (EAI). The addon uses webpack behind the scenes to keep dynamically loaded node modules off the app bundle and to mount them first when needed.

Target

Our target, in EmberJS terms, is loading a module at places dedicated primarily to handling asynchronicity — the model hooks — followed by saving the loaded functions or properties for a latter use (a simplified example below).

// app/index/route.js beforeModel() {

...

return import('libphonenumber').then((module) => {

// do sth with the loaded module

});

},

Configuration

Well, let's provide an example for what such a configuration for libphonenumber may look like.

Modify ember-cli-build.js configuration to let the EAI addon know which module to skip when bundling the vendor files.

autoImport: {

publicAssetURL: '/assets',

alias: {

libphonenumber: 'google-libphonenumber/dist/libphonenumber',

},

// avoids multiple import

exclude: ['qunit', 'moment', 'autosize', 'ldclient-js', 'rsvp'],

},

The publicAssetsURL tells the EAI where to output the module within the dist folder. Every such dependency will be (by default) fingerprinted, given a name prefixed with chunk (for example dist/assets/chunk.ed16d872df711b50f989.js ).

The alias key stands for a key we want to use in our beforeModel() hook to target the module. The value holds a location of the module within the node_modules folder. If the package name is the same as the key you want to use, there is no need to configure an alias.

Since webpack and ember-cli might bundle other node modules redundantly, you might need to explicitly exclude them via the exclude config option.

Building

During the build process (say you run $ ember build -prod ) the EAI produces a chunk per lazy-loaded module and places it into the configured path (in our case it was the dist/assets folder):

The dist folder layout after the build process

Some information related to the bundling process is revealed in the console:

Asset Size Chunks Chunk Names

chunk.536b638f912d1a992d67.js 2.76 KiB 0 app

chunk.5d120ecc1871f382581f.js 25.7 KiB 2 vendors~app

chunk.8a81c3889ccbe62637dd.js 6.64 KiB 3 vendors~tests

chunk.a19df52fb071e2603fe8.js 1.91 KiB 1 tests

chunk.ed16d872df711b50f989.js 444 KiB 4 [big] Entrypoint app = chunk.5d120ecc1871f382581f.js chunk.536b638f912d1a992d67.js Entrypoint tests = chunk.8a81c3889ccbe62637dd.js chunk.a19df52fb071e2603fe8.js ... [29] ./node_modules/google-libphonenumber/dist/libphonenumber.js 540 KiB {4} [built]

... Built project successfully. Stored in "dist/". File sizes:

- dist/assets/chunk.ed16d872df711b50f989.js: 444.26 KB (92.08 KB gzipped)

...

There is an extra argument you can use to get a deeper view on what webpack is doing:

$ DEBUG="ember-auto-import:*" ember b -prod

Clarification

Now, if you have already tweaked your model hook to import() a module on the fly, you should be able to confirm the correct behaviour in a network tab of your browser:

The module script is being fetched once the execution hits the beforeModel() hook

After this point it is up to you what you do with the fetched script. You can use it directly in the route hook, or you can store the loaded module in a property and pass to a component or save it in a service.

The screenshot demonstrates availability of the imported module in the app.

Prevention

The last question to be answered is how to prevent developers from adding a large dependency and realize it after shipping into production.

Instead of writing a custom script, we reached out for an addon called ember-cli-bundlesize. With a super simple configuration as:

'use strict'; module.exports = {

'js-app': {

pattern: 'assets/zonky-app-*.js',

limit: '200KB',

compression: 'gzip',

},

'js-vendor': {

pattern: 'assets/vendor-*.js',

limit: '500KB',

compression: 'gzip',

},

css: {

pattern: 'assets/styles/*.css',

limit: '50KB',

compression: 'gzip',

},

};

It proved to be exactly what we needed due to the fact that:

In case of a failure the command will exit with a non-zero exit code. So you can integrate this command into your CI workflow, and make your builds fail when the bundle size test does not pass.

Thus our CI pipeline ends with an error if a developer attempts to add a module whose size adds to the size of an app exceeding the configured limit.