lazy-loading-with-ember

Ember’s RC6 release is soon to be announced, and with it will land the excellent and powerful router changes from @machty. If you are using Ember today and haven’t yet reviewed the upcoming changes I strongly suggest doing so. The API is backwards compatible, but empowers new and far more interesting flows for your app.

These improvements grew out of the challenges of authentication flows. Specifically, you can now use the beforeModel hook to validate that there is a current user, and abort or redirect if one does not exist. Then after completing authentication, the prior destination can be loaded. There is an excellent and detailed embercast on how to do this.

The tl;dr on these changes is that they embrace asynchronicity and make transitions into first-class objects.

Let’s explore the improved API by building a lazy loader for app code. When a user arrives at a route, the loader will check to see if the required code has been loaded then fetch it if need be.

A LazyLoaderMixin

beforeModel is a hook on routes that fires before model itself. It is the first public API hook executed when entering a route.

The asynchronous router leverages promises quite heavily. Because of this design choice, we can easily defer a route transition by returning a promise from beforeModel, model, or afterModel. A dynamic loader mixin can define the behavior:

App.LazyLoaderMixin = Ember.Mixin.create({

  beforeModel: function(){
    var scriptName = '/js/'+this.get('routeName')+'.js';
    if (!App.LazyLoaderMixin.loaded[scriptName]) {
      return $.getScript(scriptName).then(function(){ // getScript is in jQuery
        App.LazyLoaderMixin.loaded[scriptName] = true;
      });
    }
  }

});

App.LazyLoaderMixin.loaded = [];

This code is terse only because of promises. getScript returns a promise. Calling .then on that promise allows the loader to flag the resources as fetched. .then returns a promise itself. This returned value is what the route will chain onto, ensuring the resource is fetched before Ember tries to resolve a model or render a template.

Use the mixin via extend:

App.IndexRoute = Em.Route.extend(App.LazyLoaderMixin, {
  /* Your model or other route options */
});

beforeModel will now load /js/index.js the first time a user visits that URL. index.js could define classes such as models used in the model hook, views, templates, or controllers.

I’ve put together a live demo on Github Pages:

This is obviously a naive implementation, but a good proof of concept. It even handles routes that require shared code. For instance, /cars and /cars/42 would likely both require the App.Car data model. As long as the routes are nested:

App.Router.map(function(){
  this.resource('cars', function(){
    this.route('show', {path: ':id'});
  });
});
// Presume CarsRoute is extended with the lazy loader

Then cars.js would be fetched if you land on either route. This is shown with ApplicationRoute in the live demo.

If a given resource is missing, getScript will reject it’s promise. The router can handles promise failures in the events hash, allowing you to prompt the user or provide some alternative behavior.

App.IndexRoute = Em.Route.extend(App.LazyLoaderMixin, {
  events: {
    error: function(reason) {
      alert('I cannot render that page while offline!');
    }
  }
});

This simple mixin is a start on lazy loading. The best part of this example is how little of the application code needs to change. Migrating an existing codebase would be largely a matter of understanding where your dependencies lie. With a real asset pipeline, even more doors for how to handle those dependencies open up.