Asynchronously pre-loading scripts with AngularJS and RequireJS

As your web application grows, the number and size of JavaScript files you will have to load grows as well. If you are using AngularJS and RequireJS, you might well reach a level where the initial loading of such resources takes so long that you need to start looking into better ways to handle loading these dependencies.

Initial Loading

Your starting point when working with AngularJS is that everything is loaded in the beginning:

initial loading

The advantages of this approach are that it’s very easy to handle and switching to the second or third view is extremely fast as everything was already loaded upfront.

Lazy loading

As described in this article, lazy loading can help reducing the initial loading time by loading resources on demand when the users moves to another view:

lazy loading

So with this approach the time required to switch from the initial view to another view is increasing (because you now need to load some resources first) but the initial load time is decreasing.

Asynchronous pre-loading

In order to get a low initial loading time and not require any loading time for the second and third view, you need to implement some asynchronous solution which relies on the fact that files which have already been loaded will not be loaded again and that the user usually doesn’t immediately switch to the second or third view.

The delay introduced by the user could be because he needs to enter some credential in a login page. Or the initial view is some kind of dashboard and the user will first review all displayed information before going to more detailed views.

asynchronous preloading

When the initial view is displayed, we require scripts needed for the other two views to be loaded asynchronously. So while the user is interacting with the initial view, the scripts are loaded in the background and once the user activates one of the other views, the scripts will not be loaded again.

Asynchronous pre-loading with AngularJS and RequireJS

In order to load JavaScript files asynchronously in the background, we’ll need to use the async version of require:

require(["module_name"])

There are basically 3 places where you could trigger this:

  1. Immediately
  2. In the resolve function of your route definition
  3. In the require callback of your view

The problem with the first option is that since it triggers the asynchronous background loading of the files immediately, these load operations will compete with the loading of other resources you are waiting for. This means that it will use some of the bandwidth you need for the synchronous loading of files and it will also use HTTP connections which might cause the other load operations to have to wait.

If you are dynamically loading files for the new view in your resolve function, putting the code to asynchronously load files you will need in the future has the same effect. It’s just maybe not that bad because the files which are lazy loaded might be less or smaller than the ones which are required for all views and loaded upfront.

So the solution I went for is the third. Since the background loading is triggered once the required files for the view have been loaded, it has no impact on other load operations.

I’ve updated the pluggableViews provider I’ve already described in a previous post to have an additional optional parameter called preload. It’s basically a function called in the callback of the require function call during lazy loading of the view. The default value for this function is an empty function:

if (!viewConfig.preload) {
    viewConfig.preload = function () {
    };
}

And it is called in the callback of the require function:

$routeProvider.when(viewConfig.viewUrl, {
	templateUrl: viewConfig.templateUrl,
	controller: viewConfig.controller,
	resolve: {
		resolver: ['$q', '$timeout', function ($q, $timeout) {

			var deferred = $q.defer();
			if (angular.element("#" + viewConfig.cssId).length === 0) {
				var link = document.createElement('link');
				link.id = viewConfig.cssId;
				link.rel = "stylesheet";
				link.type = "text/css";
				link.href = viewConfig.cssUrl;
				angular.element('head').append(link);
			}
			require([viewConfig.requirejsName], function () {
				pluggableViews.registerModule(viewConfig.moduleName);
				$timeout(function () {
					deferred.resolve();
					viewConfig.preload();
				});
			});
			return deferred.promise;
		}]
	}
});

I can then use it this way:

$pluggableViewsProvider.registerView({
	ID: 'walls',
	moduleName: "cards.walls",
	requirejsConfig: {paths: {'walls': '../views/walls/walls'}},
	preload: function () {
		require(['reports'], function () {
			console.log("reports loaded");
		});
		require(['admin'], function () {
			console.log("admin loaded");
		});
	}
});

Conclusion

Of course, introducing lazy loading and asynchronous background pre-loading increases the complexity of your application. And it is not trivial to introduce once you already have a large application which dependencies are a mess because you never need to clean them before (since by default AngularJS causes all files to be loaded upfront).

But if you load lots of files (especially JavaScript libraries), then making sure that files are loaded when needed and are not loaded at a point in time where this would slow down the application, will definitely help making your application look thinner and faster (from a user perspective).

Leave a Reply

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