lazy-loading-with-ember
Wow! This takes me back! Please check the date this post was authored, as it may no longer be relevant in a modern context.
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.