When building large sites or apps with many routes/views in AngularJS, it would be good to not have to load all artefacts such as controllers, directives etc., on the first load. Ideally, on first load, only the artefacts that are needed for the route in question, will be loaded. This may be either in one download or multiple depending on the app, however, it will only be what is needed to render the particular route. Then as the user navigates the app by changing the route, other artefacts that have not already been loaded, will then be loaded as and when there are needed. Not only should this potential speed up the initial page load, but it should also result in bandwidth not being wasted. With this post, my aim is to show how the lazy loading of artefacts such as controllers and directives can be achieved with AngularJS.
In order to lazy load artefacts such as controllers and directives in AngularJS, there are two primary questions that will have to be answered and they are as follows:
- How can lazy artefacts be registered against a module after the application has already been bootstrapped?
- Where in the application should the actual loading take place using your script loader of choice?
The first question results from a current inability to register artefacts after application bootstrap, using the module API. In other words, if you were to try to register a new controller with an already bootstrapped app, using the following code:
you would get the following error when you reference the controller with the ng-controller
directive:
Error: Argument ‘SomeLazyController’ is not a function, got undefined
Currently, the only way (that I know of) to register artifacts with an already bootstrapped application, is not to use the module API, but to use the relevant AngularJS provider instead.
Providers are essentially objects that are used to create and configure instances of AngularJS artefacts. Hence, in order to register a lazy controller, you would use the $controllerProvider. Similarly, To register a directive, you would use the $compileProvider, to register filters you would use the $filterProvider, and to register other services, you would use the $provide service. The code will look something like this for controllers and directives:
Now the thing with providers is that they are only available during module configuration. Hence, a reference to them will have to be kept so that they can be used later on to register lazy artefacts. As an example, to get a hold of the relevant providers, you could setup your app module similar to the following:
You would then be able to define a lazy controller as follows:
The question still remains, however, as to where the loading of lazy artefacts such as the above controller, will take place using your script loader of choice. Currently, there is only one place where this can happen ‘cleanly’ and it is in the ‘resolve’ property of the route definition.
When defining a route using the $routeProvider, you can specify an optional key/factory map of dependencies that should be injected into the route controller. This dependency map is specified using the ‘resolve’ property as follows:
The ‘key’ in the dependency map will be the name of the dependency, and the ‘factory’ will either be a string that is the alias of an existing service that should be used as the dependency, or an injectable function whose return value should be used as the dependency. Now if the function returns a promise, the promise will be resolved before the route is ‘rendered’ (so to speak). Thus, dependencies that have to be retrieved asynchronously, such as lazily loaded artefacts, can be retrieved using a dependency map function that returns a promise that will be resolved once the lazy artefacts have been loaded. This ensures that all lazy artefacts are loaded before the route is rendered. An example of a route definition that specifies lazy dependencies to be loaded using the $script.js script loader, is as follows:
One thing that should also be noted is that because the promise resolution will most likely be taking place outside the context of AngularJS, as in the above example, it is happening inside the $scriptjs context, AngularJS has to be explicitly told when the promise has been resolved (so to speak). This is achieved by resolving the promise inside of the $apply method of the $rootScope as in:
If the promise is not resolved inside the $apply method of the $rootScope, the route will not be rendered on the initial page load.
Now applying all this back to the app module definition, will yield the following:
Finally you can bootstrap the app using code similar to the following if using $script.js:
These are more or less the steps that you would take to implement lazy loading in AngularJS. In summary, you would first define your app module to keep instances of the relevant providers. Then you would define your lazy artefacts to register themselves using the providers rather than the module API. Then using a ‘resolve’ function that returns a promise in your route definition, you would load all lazy artefacts and resolve the promise once they have been loaded. This ensures that all lazy artefacts will be available before the relevant route is rendered. Also, don’t forget to resolve the promise inside $rootScope.$apply, if the resolution will be happening outside of AngularJS. Then you would create a ‘bootstrap’ script that first loads the app module before bootstrapping the app. Finally, you would link to the bootstrap script from your ‘index.html’ file.
To see a runnable example using Asynchronous Module Definitions with RequireJS, have a look at the sample app.