Tuesday, November 4, 2014

Lazy loading directives in AngularJS the easy way


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 problems 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) {
    
    // This is a dictionary that holds which directives are stored in which files,
    // so we know which file we need to download for the user
    var _directivesFileMap = {
        'SexyDirective': 'scripts/directives/sexy-directive.js'
    };

    var _load = function(directiveName) {
        // make sure the directive exists in the dictionary
        if (_directivesFileMap.hasOwnProperty(directiveName)) {
            console.log('Error: doesnt recognize directive : ' + directiveName);
            return;
        }

        var deferred = $q.defer();
        var directiveFile = _directivesFileMap[directiveName];

        // download the javascript file
        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) {
    
    // lets say we want to load our 'SexyDirective' all we need to do is this :
    LazyDirectiveLoader.load('SexyDirective').then(function() {
        // now the directive is ready...
        // we can redirect the user to a page that uses it!
        // or dynamically add the directive to the current page!
    });

}]);


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 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

9 comments:

  1. This comment has been removed by a blog administrator.

    ReplyDelete
  2. If the user will use Cloud computing virtual data room solutions , it will not have a problem with HUGE amounts of js files being downloaded to the user's computer

    ReplyDelete
  3. I have read your blog its very attractive and impressive. I like it your blog.

    Angularjs Online Training Angularjs Training Angularjs Training Angularjs Training in Chennai Angularjs Training in Chennai

    ReplyDelete
  4. Thanks for posting this useful content, Good to know about new things here, Let me share this,
    AngularJS Training in Chennai | AngularJS Training | Best AngularJS Training Institute in Chennai

    ReplyDelete
  5. Thanks for sharing this informative content that guided me to know the details about the training offered in different technology.Best Angularjs Training in Chennai
    AngularJS Training in Chennai

    ReplyDelete
  6. Attractive blog post! I should read from some useful news for this blog section, It 's a great content Keep it useful sharing.
    angularjs training in chennai , best angularjs training course in chennai | selenium training in chennai |best selenium course in chennai

    ReplyDelete