Skip to content

Latest commit

 

History

History
139 lines (89 loc) · 7.81 KB

0000-lazy-load-overview.md

File metadata and controls

139 lines (89 loc) · 7.81 KB
  • Start Date: 2016-04-13
  • RFC PR: (leave this empty)
  • ember-cli Issue: (leave this empty)

Summary

Provide an overview of the different parts required to enable different lazy-loadins scenarios for Ember Applications.

Motivation

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.

Detailed design

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.

Building

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.

Building multiple CSS:

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',
      }
    }
  }
});

Building multiple vendor JS assets from static files (e.g. bower/vendor):

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'});

Building multiple vendor JS assets from addons

There is an RFC for this, but the concept is similar to the above, but extended for addons.

Building multiple JS assets for app code

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.

Loading assets

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.

Lazy loading from components

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');

Lazy Loading from routes

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);
  });
};

Route metadata

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

Engines can simpy rely on all of the parts above to provide nice conventions.

Dependencies and concatenation

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.

Pre-requisites

Serialize RFC

Drawbacks

TODO: fill this in

Alternatives

TODO: fill this in

Unresolved questions

TODO: fill this in, but the whole RFC is already full of unresolved questions, some of which deserve their own RFC