Single Page Apps: bulimia risk (AngularJS)

The initial loading of big apps comes at a price

The Single Page Application concept, that AngularJS is a classical way to use for, comes fast to a major issue. Indeed, as soon as we are working on a consequent application, the amount of Javascript (and eventually of CSS) required by the application on first loading can become a pain.

The consequence is simple : whereas Single Page Apps aim to offer a very fast UX for visitors, the initial loading can be very detrimental.

However, a visitor will most probably use only a very few parts of the application. We came rapidly to this observation on our service www.dareboost.com, with a pretty much clear segmentation, since an important part of the traffic was using the solution anonymously, without any access to the login-required part of the app. So, why make this public to download large amounts of unused data?

On-the-fly scripts injection, yes but…

In theory, the answer to this weight issue seems kind of easy: load only elements that are immediately useful or very soon to be. We can load the remaining later, according to the needs of the navigation.

Looking a bit more deeply in this first idea brings another issue: AngularJS requires all the Javascript code at application’s initialization (modules, controllers, services, directives, etc)

If you try to load a Javascript file by injecting a <script> tag “on-the-fly” as shown below:

var anchor = document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0];
	var insertStyle = function (path) {
	var el = document.createElement('script');
	el.src = path;
	el.async = 1;
	anchor.insertBefore(el, anchor.lastChild);
}

The web browser will load and parse the code. but AngularJS will not take into account this script, since to do so, the framework needs its providers mechanisms to be used (allowing to register modules, controllers and others).

The solution: dependency management for modules and LazyLoading

If your app is already divided in several “standalone” modules, you can easily associate one (or more) module to one (or more) URL/page.

Thus, when loading a new page you just have to load required modules (NB: Javascript code related to a template must be taken into account by AngularJS before it can be displayed).

One finds well the general concept of LazyLoading, we only load the required elements.

The oclazyload module (https://github.com/ocombe/ocLazyLoad) helps a lot to implement this mechanism. It allows especially to configure the loading of the different application’s parts, directly in the route scheme settings.

Some code samples for understanding

As an example, we’ll take a webmail application, that we’ll divide in 3 modules:

  • The inbox
  • Email writing/sending
  • Address book

Each of these 3 modules is related to his own javascript file (and CSS file eventually,  containing styles dedicated to the pages of the given module).

Moreover, we’ll need a main module that will systematically be loaded, and that will manage global features of the application (permissions management, or error messages management for example)

Module name JavaScript file CSS File
inbox inbox.min.js inbox.min.css
create create.min.js create.min.css
contacts contacts.min.js contacts.min.css

We have yet to configure modules and their dependencies in ocLazyLoad config:

$ocLazyLoadProvider.config({
	// main application module
	loadedModules: ['app'],
	// defining lazy loaded modules and association to their dependencies
	modules: [
		{name: 'inbox', files: [inbox.min.js', ‘inbox.min.css']},
		{name: 'create', files: ['create.min.js', ‘create.min.css']},
		{name: 'contacts', files: ['contacts.min.js', ‘contacts.min.css']},
	],
});

Each module is now associated to its dependencies. Requesting ocLazyLoad to load a module is as simple as a call to load(moduleName) method.

To ensure that a module is loaded before a template (required for directives for example), we include a call to load method when defining route schemes.

Routes (without lazy loading):

$routeProvider.when('/inbox', {
     templateUrl : '/fragment/inbox’,
      controller: 'InboxCtrl',
})
.when('/write', {
     templateUrl : '/fragment/create’,
      controller: 'CreateCtrl',
})
.when('/contacts', {
     templateUrl : '/fragment/contacts’,
      controller: 'ContactsCtrl',
});

LazyLoad enabled routes:

$routeProvider.when('/inbox', {
     templateUrl : '/fragment/inbox’,
      controller: 'InboxCtrl',
resolve: { 
// Any property in resolve should return a promise 
// and is executed before the view is loaded
        loadModule: ['$ocLazyLoad', function($ocLazyLoad) {
            // you can lazy load files for an existing module
            return $ocLazyLoad.load('inbox');
        }]
    }
})
.when('/create', {
     templateUrl : '/fragment/create’,
      controller: 'CreateCtrl',
resolve: { 
loadModule: ['$ocLazyLoad', function($ocLazyLoad) {
            return $ocLazyLoad.load('create');
        }]
    }
})
.when('/contacts', {
     templateUrl : '/fragment/contacts’,
      controller: 'ContactCtrl',
resolve: { 
loadModule: ['$ocLazyLoad', function($ocLazyLoad) {
        return $ocLazyLoad.load('contacts');
    }]
    }

});

Javascript code related to each module being only loaded when a new route is taken (or eventually when an explicit call to ocLazyLoad.load is done), we have to change the way to declare application’s main module: lazyloaded modules must not be declared as dependencies here (since they are not known at this stage).

Classical way, without lazy loading :

angular.module('dareboost', [ 'ngRoute', 'create', 'inbox', 'contacts']);

With lazy loading

angular.module('dareboost', [ 'oc.lazyLoad', 'ngRoute']);

Some points you may want to know about:

Within our website analysis tool we encountered two issues when starting to use ocLazyLoad:

  • Javascript/CSS dependencies shared by several modules were potentially loaded several times. But great news, this problem is now fixed natively by the module!
  • “live” CSS on the web browser is dependent of the navigation path followed by the user. Whereas on a classic website we reload everything on each page, Single Page Apps with modular divided CSS will encounter a phenomenon of live CSS “accumulation” during the user navigation. It can lead to some conflicts if you do not pay attention!

Go further :

Keep speaking about LazyLoading, we implemented on DareBoost a mechanism to delay DOM production within directives: the DOM is generated on-demand, when the user is scrolling. It may be the topic of a further post on our blog!

One thought on “Single Page Apps: bulimia risk (AngularJS)

  1. Nice article! Really enjoyed it! I want to point out another issue, which was parallel loading. If you had to load two files which are dependent on one another, sometimes you get an error. This issue was solved however using the field serie inside object items in the modules array.
    modules: [{
    name: ‘moduleName’,
    files: [
    “file1.module.js”,
    “file1.controller.js”
    ],
    serie: true
    }]

Leave a Reply

Your email address will not be published. Required fields are marked *

*