A lazy loading solution for Angular 1.x

In this post, I’m going to show you a solution of lazy loading Angular 1.x modules. Angular 1.x doesn’t have it supported out of the box and it’s a very critical feature for many large applications dealing serious businesses.

The demo project used for this post can be found from here https://github.com/jack4it/angular-1x-lazy-load.

Aren’t this problem already solved by Angular 2 and Aurelia?

Some of you might ask, given that Angular 2 is already in beta stage, and also there is another even better framework called Aurelia almost ready for its first release, why do we still need to care about Angular 1.x? There indeed are some valid reasons for that.

  • Many existing Angular 1.x projects will just not migrate to the new framework
  • Both Angular 2 and Aurelia are just in beta stage and it’ll take time for the majority to be confident enough to start to use them on new critical projects
  • etc.

So this solution will still be helpful for at least a while.

And a bonus point, in this solution, I’m also gonna show you how to write ES6/ES2015 codes and use systemjs loader even for today’s Angular 1.x projects. Another bonus point, the lazy loaded modules are also well bundled using systemjs-builder. So that you can have a seamless workflow for both development and production environments.

In the rest of this post, if not explicitly declared, by the term Angular, I’ll just mean Angular 1.x.

Why does it matter?

It’s funny that Angular fosters modular design/separation of concern for large client applications, but doesn’t provide a lazy loading story. The module meta language it provides is far from ideal, but it still works (plain ES6/ES2015 module is the one true king of module kingdom).

Modular design helps with a lot of things including team collaboration, maintainability, and etc. But it doesn’t really help in production if the good modules all have to be loaded entirely beforehand for the app to run.

In reality, we want to load only the needed modules initially for a faster boot experience and lazily load the other modules when user triggers the related functionality of the app. And this really matters for most serious applications regarding performance.

All right then, how?

So you are still interested in this offering. Great, let’s get to the details. In order to achieve this lazy loading goal, three problems have to be solved:

  1. When, where and how is a module going be triggered to load?
  2. How is a module going to be actually loaded?
  3. Once the module is loaded, how should it be registered to Angular, so that it can be used down the road?
I’ll give all answers to these three questions later in following sections. But first let’s imagine a demo project, so that we can code it up and it’ll be much easier to see the real working codes than just read a dry post.

The little demo project

We’ll have this structure for the demo. Logically the app will have a homepage (the initial load) where we can link to other two lazy-loaded pages (powered by Angular). They are the contact page and about page.

The app.* will serve for homepage purpose as the main entry point of the app. In each lazy-loaded module, we’ll have all their Angular resources defined in a self-contained way and wire them all up in the respective module.js which you’ll see later also serves the purpose of bundling point.

2015-12-29_12-51-27

Without further due, let’s get to resolve the three problems to lazy load Angular modules.

The trigger

In a JavaScript client app, it usually takes a router component to serve the navigation purpose. It is natural to think if we can somehow extend the router, then we can trigger the actual loading when a navigation is requested and register the loaded modules to Angular. And this is indeed true for our solution. We’ll use ui-router to easily define the lazy loading points and seamlessly wire up with systemjs to do the actual loading work.

We favor ui-router over ng-route because it provides more convenient ways of providing lazy loading support which in turn comes from the ui-router-extras project, the future states. Following is a snippet of how the wire-up looks like.


export let app = angular.module("app", ["ui.router", "ct.ui.router.extras"])
.service("SystemLazyLoadService", SystemLazyLoadService)
.config(["$stateProvider", "$urlRouterProvider", "$futureStateProvider", ($stateProvider, $urlRouterProvider, $futureStateProvider) => {
$stateProvider.state('home', {
url: "",
template: template
});
$urlRouterProvider.otherwise("");
$futureStateProvider.stateFactory("systemLazy", ["SystemLazyLoadService", "futureState", (loadService, futureState) => {
return loadService.load(futureState.src, futureState.moduleExportKey);
}]);
// These are the lazy module (future state) declarations
addSystemLazyState($futureStateProvider, "contact", "/contact", "contact/module.js", "contact");
addSystemLazyState($futureStateProvider, "about", "/about", "about/module.js", "about");
}])
function addSystemLazyState($futureStateProvider, stateName, url, src, moduleExportKey) {
$futureStateProvider.futureState({
stateName: stateName,
url: url,
type: "systemLazy",
src: src,
moduleExportKey: moduleExportKey
});
}

The key pieces to notice in the above snippet are:

  • A state factory called systemLazy is created by using $futureStateProvider.stateFactory function. This state factory delegates the state preparation (the lazy loading) to a service called SystemLazyLoadService. More on the details of this service in next section
  • Then we add two future states, the contact and about modules using function addSystemLazyState which in turn calls function $futureStateProvider.futureState. Notice how we take care of the state name, the routing Url, the source location of the JavaScript module and optionally the export key of the Angular module (respectively contact and about found in the module.js files)

The loading and registration

Now let’s talk about the actual module loading and the registration of the loaded Angular module. As I mentioned above, this is achieved by the SystemLazyLoadService which looks like below snippet.


export class SystemLazyLoadService {
static $inject = ["$ocLazyLoad"];
constructor($ocLazyLoad) {
this.$ocLazyLoad = $ocLazyLoad;
}
load(src, moduleExportKey) {
let loader = this.$ocLazyLoad;
return System.import(src)
.then(module => {
var angularModule = module[moduleExportKey || 'default'];
if (!angularModule) {
console.info(module);
throw new Error("Unexpected angular module");
}
return loader.load(angularModule);
})
.then(() => {
return null; // !!! critical here; this is needed to trick future state infra
});
}
}

You may noticed that this is just a regular ES6/ES2015 module which is also registered as an Angular service. The logic is fairly straightforward. It mainly does two things:

  1. Loading: On line 11, we are doing System.import and let systemjs take care of the actual loading business. Thanks to the great systemjs loader, this single line of code is all we need for the loading part
  2. Registration: Once the module is loaded back via systemjs, the next big thing is to register the module into Angular, so that we can use the module down the road. We are using a nice library called ocLazyLoad to take of this part of the business. Again, while it is just one line of code on line 18, ocLazyLoad is actually doing a lot of work behind the scene. With ocLazyLoad’s help, we can stay away from dealing with Angular’s variety of providers to register all lazy loaded Angular resources

The last and important matter: bundling

Now we have solved the three problems in order to enable lazy loading of Angular modules. By integrating all these libraries, we now can seamlessly define the lazy loading points and load the respective module only when it is needed. Nice, but there is one last very important thing before we can call this solution complete. It is the bundling. As I mentioned above, the well crafted modules will not help in a production environment if we don’t have a bundle story.

By using systemjs-builder, we have also achieved this goal easily. Following is an excerpt of the bundle.js file you can find from the demo project.


var appRoot = "/";
var Builder = require('systemjs-builder');
// optional constructor options
// sets the baseURL and loads the configuration file
var builder = new Builder("/", 'config.js');
function build(entry, output) {
var message = entry + " –> " + output;
var begin = new Date();
console.log("—- Build started @ " + begin.toLocaleTimeString() + " # " + message);
builder
.bundle(entry, output, {
minify: true,
mangle: true
})
.then(function (output) {
var index = 1;
output.modules.forEach(function (m) {
////output.modules.sort().forEach(function (m) {
console.log(" #" + index++ + " " + m);
});
logEnd(begin, message);
})
.catch(function (err) {
console.log('!!! error');
console.log(err);
logEnd(begin, message);
throw err;
});
}
function logEnd(begin, message) {
var end = new Date();
console.log("—- Build completed @ " + end.toLocaleTimeString() + " (" + (end begin) + " ms) # " + message);
}
build(appRoot + 'app.js', __dirname + '/build/app-bundle.js')
build(appRoot + 'contact/module.js', __dirname + '/build/app-bundle-contact.js')
build(appRoot + 'about/module.js', __dirname + '/build/app-bundle-about.js')

view raw

bundle.js

hosted with ❤ by GitHub

Notice at the bottom of the script we have three separate bundles generated, namely the app entry point (the initial loading), the contact module and the about module. These modules are corresponding to the future states defined in app.js.

Following is a config sample to enable the usage of the generated bundle files. With this config, systemjs will be able to load the bundles instead of the actual individual module files.


System.config({
bundles: {
"build/app-bundle.js": ['app.js'],
"build/app-bundle-contact.js": ['contact/module.js'],
"build/app-bundle-about.js": ['about/module.js']
}
});

Summary

In this post, I presented a solution to enable lazy loading for Angular 1.x modules. This solution will help a lot regarding app boot performance when the app functionalities grow along the road.

While the next generation JavaScript frameworks like Angular 2 and Aurelia are great and almost ready to release, I see there are still a large base of existing apps that will just stay with Angular 1.x and this lazy loading solution can be of a great support for their maintenances.

The accompanied demo project can be found from here https://github.com/jack4it/angular-1x-lazy-load.

Hope this helps,

-Jack

A lazy loading solution for Angular 1.x

3 thoughts on “A lazy loading solution for Angular 1.x

  1. Watermelonsg says:

    I was planning to develop a app for Android and IOS, Jackma just popped out of my mind, and I still remember you have a website jackma. net which is updated with happenings although I didnt really follow it after graduation. How are you Jack? Do you still remember me? You once gave a guessbook for my website fly2cn. com. I am watermelonsg from Singapore. We once chat through QQ.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s