- Start Date: 2016-04-13
- RFC PR: (leave this empty)
- ember-cli Issue: (leave this empty)
Provide an overview of the different parts required to enable different lazy-loadins scenarios for Ember Applications.
Lazy loading helps us optimize boot time, by minimizing the amount of code that is loaded and evaluated during boot. This allows apps to grow horizontally or support different roles without necessarily increasing the app's payload or the cost associated with it.
Lazy loading is complex and covers multiple scenarios. In order to simplify its implementation I'm breaking it in three, independent parts, all of those could be delivered incrementally and still provide value. We need to address building different assets, a standard way to load and track assets and hooks to lazy load code from routes and components. While not required, it is important to consider asset dependencies and bundling.
We need to change the way we build our assents. By default Ember CLI bundles everything as a single unit, in order to be able to lazy-load assets, we need to create more than the standard assets (app and vendor). This is partially possible today, but further enhancements are required.
Today we have two ways of building multiple CSS. For vendor static assets (from vendor/
or bower_components
) we could simply import them to a different outputFile
as specified on RFC#28 and supported since ember-cli 2.4.
app.import(app.bowerDirectory + '/bootstrap/dist/css/bootstrap.css', {outputFile: 'vendor2.css'});
For files that need a pre-processor we can use outputPaths
. In the example below, styles/deferred-styles.scss
is simply a file with references to other CSS or SASS files. Thisi works for vendor
and app
styles.
let app = new EmberApp(defaults, {
outputPaths: {
app: {
css: {
'app': '/assets/css/app.css',
'deferred-styles': '/assets/css/deferred-styles.css',
}
}
}
});
For vendor static assets (from vendor/
or bower_components
) we could simply import them to a different outputFile
as specified on RFC#28 and supported since ember-cli 2.4.
app.import('vendor/dependency-1.js', { outputFile: 'assets/alternate-vendor.js'});
There is an RFC for this, but the concept is similar to the above, but extended for addons.
This isn't supported out of the box today, but it's possible to do. There're two example, ember-cli-bundle-loader and ember-lazy-loader. Both approaches are non-standard. The first one uses multiple ember-apps and the latter uses heavy configuration to specify which app files will go to each asset.
This RFC is limited to provide an overview of what is required, so this topic requires a bit more discusion on a separate RFC. TODO: create the separate RFC. Engines can provide a nice default, where we can have a convention of one asset per engine when lazyLoading is enabled, which can be overriden to allow for one or more engines per file.
ember-cli-bundle-loader provides a service to dynamically load CSS and JS. This service will be extracted to its own add-on and extended to support the asset metadata described here.
The service can be used by components, for example when they need to load vendor JS or CSS assets. We have an example of this in Zenefits, where some of our components that depend on third-party libraries lazy-load them. In this particular case, we load the code on init and certain functions of the component are only available after the library is loaded. Most of the code is removed to focus on the lazy-loading parts.
// app/components/z-table.js
export default Ember.Component.extend({
lazyLoader: Ember.inject.service()
init() {
// This call is idempotent and the path will be mapped to a finger printed veresion
this.get('lazyLoader').load('hands-on-table.js');
}
});
// app/components/z-table.hbs
{{#if lazyLoader.loadedLibraries.handsOnTable}}
{{!-- display some buttons that are only enbled when the library is loaded--}}
{{/if}}
// ember-cli-build.js
let app = new EmberApp(defaults, {});
app.import('bower_components/hands-on-table/dist/hands-on-table.js', {outputFile: 'hands-on-table.js');
For app code, we likely want an integration with the router, for that can rely on this service to do the actual loading.
When splitting the app's JS and CSS into different assets related to different routes, we want to load the code dynamically. This today poses additional challenges becuase the handler (route) needs to exist to initiate a transition, but that route class doesn't exist until we load the code. Also link-to depends on the route information in order to serialize
the model information to create the URL. That means that even before attempting to transition the route needs to be available. The latter problem was solved with the serialize changes. To add the ability to load code and lazy load routes, we need a new hook in Ember's router or router.js, ongoing work for this is being done at the moment to make the getHandler
function asynchronous. Once that is done, we can call the loading-service similar to what we do in components and resolve with the name of the asset to be laoded based on route metadata. The following code is just an example and assumes we have avaialble the route metadata and the loadingService. A final implementation of how this hook will work is out of the scope of this RFC
Router.getHandler = function (routeName) {
let assetName = routerMetadata.getAssetFor(routeName)
if (lazyLoaderService.loadedLibraries["assetName"]) {
return this._super(...arguments);
}
return lazyLoadingService.load(assetName).then(()=> {
this._super(...arguments);
});
};
The following code assumes that we have a routerMetadata available. The definition of this needs further discussion outside of the scope of this RFC. This is something that can be done at compile time, have an DAG and use it to resolve routeNames to assetNames that can then e sued by the lazyLoaderService
.
Engines can simpy rely on all of the parts above to provide nice conventions.
Sometimes an asset has a dependency on another asset. For example, ember-cli-bundle-loader allows you to specify dependencies bettween what they call "bundles", the lazyLoaderService
can make sure that the dependencies are loaded in parallel, but evaluated in the correct order.
Another optimization is to concatenate dependencies into a single asset to avoid multiple requests. Today we can solve it during the build process by using the same outputFile
, for engines and app code we might need to override some of the build defaults to allow the user to concatenate the engine's output to minimize the number of requests.
TODO: fill this in
TODO: fill this in
TODO: fill this in, but the whole RFC is already full of unresolved questions, some of which deserve their own RFC