CodeProject
During the past few months, I've been doing a lot of work with AngularJS, and currently I'm working on a single page application which is supposed to be quite big in the end. Since I have the privilege of building it from scratch, I'm taking many client-side performance considerations in mind now, which I think will save me a lot of hard work optimizing in the future.
One of the main problems is HUGE amounts of js files being downloaded to the user's computer. A great way to avoid this is to only download the minimum the user needs and dynamically load more resources in the background, or as the user runs into pages which require a specific feature.
AngularJS is a great framework, but doesn't have anything built in that deals with this, so I did some research myself...
I ran into some great articles on the subject, which really helped me a lot (and I took some ideas from), but weren't perfect.
A great article on the subject is this one: http://www.bennadel.com/blog/2554-loading-angularjs-components-with-requirejs-after-application-bootstrap.htm
The important part is that it explains how to dynamically load angularjs directives (or other components) after bootstrapping your angularjs app.
What I didn't like about this article is that the writer's example requires RequireJS and jQuery along with all the AngularJS files you already have. This alone will make your app really heavy, and I think doesn't need to be like this.
Let me show you how I wrote a simple AngularJS service that can dynamically load directives.
The first crucial step is that you need to save a reference to $compileProvider
. This is a provider that is available to us when bootstrapping, but not later, and this provider will compile our directive for us.
var app = angular.module('MyApp', ['ngRoute', 'ngCookies']);
app.config(['$routeProvider', '$compileProvider', function($routeProvider, $compileProvider) {
$routeProvider.when('/', {
templateUrl: 'views/Home.html',
controller: 'HomeController'
});
app.compileProvider = $compileProvider;
}]);
Now, we can write a service that will load our JavaScript file on demand, and compile the directive for us, to be ready to use.
This is a simplified version of what it should look like:
app.service('LazyDirectiveLoader',
['$rootScope', '$q', '$compile', function($rootScope, $q, $compile) {
var _directivesFileMap = {
'SexyDirective': 'scripts/directives/sexy-directive.js'
};
var _load = function(directiveName) {
if (_directivesFileMap.hasOwnProperty(directiveName)) {
console.log('Error: doesnt recognize directive : ' + directiveName);
return;
}
var deferred = $q.defer();
var directiveFile = _directivesFileMap[directiveName];
var script = document.createElement('script');
script.src = directiveFile;
script.onload = function() {
$rootScope.$apply(deferred.resolve);
};
document.getElementsByTagName('head')[0].appendChild(script);
return deferred.promise;
};
return {
load: _load
};
}]);
Now we are ready to load a directive, compile it and add it to our app so it is ready for use.
To use this service, we will simply call it from a controller, or any other service/directive like this:
app.controller('CoolController', ['LazyDirectiveLoader', function(LazyDirectiveLoader) {
LazyDirectiveLoader.load('SexyDirective').then(function() {
});
}]);
One last thing to notice is that now your directives need to be defined using '$compileProvider
', and not how we would do it regularly. This is why we exposed $compileProvider
on our 'app
' object, for later use. So our directive js file should look like this:
app.compileProvider.directive('SexyDirective', function() {
return {
restrict: 'E',
template: '<div class=\"sexy\"></div>',
link: function(scope, element, attrs) {
}
};
});
I wrote earlier that this is a simplified version of what it should look like, since there are some changes that I would make before using it as is.
First, I would probably add some better error handling to look out for edge cases.
Second, we wouldn't want the same pages to attempt to download the same files several times, so I would probably add a cache mechanism for loaded directives.
Also, I wouldn't want the list of directive files (the variable _directivesFileMap
) directly in my LazyDirectiveLoader
service, so I would probably create a service that holds this list and inject it the service. The service that holds the list will be generated by my build system (in my case, I created a gulp task to do this). This way, I don't need to make sure this file map is always updated.
Finally, I think I will take out the part that loads the JavaScript file to a separate service so I will be able to easily mock it in tests I write. I don't like touching the DOM in my services, and if I have to, I'd rather separate it to a separate service I can easily mock.
I uploaded a slightly better (and a little less simplified) version of this over here: https://github.com/gillyb/angularjs-helpers/tree/master/directives/lazy-load