Tuesday, July 7, 2015

non-blocking CSS loading using AngularJS

Ever wanted to load a lot of css, without blocking the browsers rendering of the page?

the simplest way to do that using angular is make the entire page an app, and slap an ng-href on your like like this:

 <link ng-href="/someCssFile.css" rel="stylesheet">

That line alone makes sure the CSS is only loaded, after angular has started. That should add some points to your pagespeed index already! However, if not taken care of, there will be a FOUC shown to your user. Your app might kick in, but not (all) the css is already loaded. Read this blog from Ilya Griorik for more details

It works, because ng-href means nothing to the browser, so the link tag is ignored. However, once angular kicks into action, it evaluates the ng-href, and updates the links href property. By doing this, it will start loading the css, after angualr has started.

If you want to take this a step futher, create a small directive like this:
    angular
        .module('sePreloadCss', ['sePreload'])
        .directive('seLazy', seLazy);

    /* @ngInject */
    function SeLazy (PreloadRegistery) {
        var directive = {
            controller: LazyController,
            restrict:   'A',
            // make sure this is ready before the src kicks in.
            priority:   1000
        };
        return directive;

        /* ngInject */
        function LazyController($element,$attrs) {
            var rel = $attrs.rel;
            var elm = $element[0];
            if (typeof rel !== 'string' || 
                rel.toLowerCase()!=='stylesheet' ||
                typeof $attrs.ngHref !== 'string') {
                return;
            }
            PreloadRegistery.register(elm);

            this.loaded = function loaded() {
                console.log(elm.href + " finished loading!")
                PreloadRegistery.done(elm);
            };
            
            elm.addEventListener("load", this.loaded);

        }
    }
Complemented by a service:
    angular
        .module('sePreload', [])
        .factory('PreloadRegistery', PreloadRegistery);

    /* @ngInject */
    function PreloadRegistery($q) {
        var map      = new Map();
        var loadDone = $q.defer();
        var service  = {
            register: register,
            done:     done,
            loadDone: loadDone.promise,
            has:      has
        };
        return service;
        ////////////////

        function register(elm) {
            map.set(elm, false);
        }

        function has(elm) {
            return map.has(elm);
        }

        function done(elm) {
            var allDone = true;
            map.set(elm, true);
            map.forEach(function (isDone, key) {
                if (!isDone) {allDone = false; }
            });
            if (allDone) {
                loadDone.resolve(true);
            }
        }
    }

Now you can do the following in your appController (the first controller in your app that gets started, even before routing!)
    angular
        .module('myModule',["sePreload", "sePreloadCss"])
        .controller('AppController', AppController);

    /* @ngInject */
    function AppController($q, PreloadRegistery, someTable) {
        var vm   = this;
        vm.title = 'AppController';
        vm.side  = $mdSidenav;

        $q.all([
              someTable.loadDone,
              PreloadRegistery.loadDone
            ])
            .then(activate);
        return;

        function activate () {
          vm.appReady = true;
          routerConfig();
        }

And modify your link a little bit like this:
 <link se-lazy ng-href="/someCssFile.css" rel="stylesheet">

and then finally in your template you can do something like this: (you can use ngCloak alongside!)
   <body ng-controller="appController as vm">
      <div ng-hide='vm.appReady'> 
           Your app assets are being loaded.... 
           (perhaps a nice CSS spinner inserted here!) 
      </div>
      <div ng-show='vm.appReady' style="display:none"> 

           Yay!, my app is ready, and can be shown here!
      
      </div>

I did build a small showcasing plunk. There you can see all this in action for yourself

A serious note, If one of the CSS files fails to load, the spinner will be there forever. This should be handled by your app. There is no one-fits-all solution for that. In some cases, you want to go on without the css, but if it's critical to your app, you might want to warn the user, or let the app send you an alert, or.....

By doing all this, you will gain quite a couple of page-speed point. Your page will load faster, and you have more control over what gets displayed at what time. But more important, you can inform your user what is going on, and they won't witness a lot of funky things going on. I created this, because I was building a page that uses Angalar Material Design, and FontAwesome. Also I needed to preload quite some data. This trick enabled me to show something to the user instantly, in stead of displaying a blank page for 4 to 10 seconds.

No comments: