Introduction

Angular 2 was developed with more awareness of the changing world of web development. It was developed to allow for easy integration of modules that can be loaded on demand with the idea of bundle splitting in mind. But what about those of us stuck in Angular 1.x land? We have all the benefits of webpack and its code splitting tools, but on a framework that had no notion of dynamic module support.

It’s apparent that Angular 1.x is not going away, as is illustrated here by the NPM downloads per week. The Angular NPM package is at a nearly all time high for weekly downloads, at the time of writing this post.

Luckily, I was given a few weeks to find a potential solution!

Why Dynamically Load Angular Modules?

Our application suffered from an initial loading time of over 10 seconds. This was due to the large bundle size, 16 MB, created by a complicated application made up of multiple sub projects.

Our initial approach was to take the bundle and split it, but still pulling in the entirety of the script regardless of whether or not the user ever accessed certain parts. This has some gains, as we can now have more asynchronous calls being made at once. However, we still experienced long loading times (4+ seconds).

Splitting the code into many segments, alone, is not a proper solution. Most browsers have a limit on the number of asynchronous requests being made at once. For instance, Internet Explorer 11 allows for 8 simultaneous requests to a single hostname, but Chrome allows for 6. Each additional request has an associated overhead with it.

We chose to define which module needed to load based on the URL, or the UI-Router state name, and load that on demand. This allows for us to bring in 1 bundle representing the shared dependencies (think Angular, ui-router, etc), and another bundle representing the sub-part of the application requested.

Our loading times were cut from over 10 seconds, to roughly 1.5 seconds. In addition, our network traffic saw several megabytes of improvement, considering some modules had dependencies on large libraries that others did not. Those dependencies could be isolated and loaded on demand.

How Angular Loads Modules

I discovered a library that attempted to solve this problem. At the time, they did not support Angular 1.5 and the author has made it clear that support will be dropped. In addition, the library was doing a lot more work than what we require (for instance, it adds script tags to the page, whereas webpack does that for us).

The guts of their code led me to a property on an Angular module called the _invokeQueue. This, as it turns out, led me to Angular’s inner workings surrounding module initialization.

At a high level, Angular will synchronously execute the initialization of each of the modules. This consists of going into each declared dependency module, and executing all of the services, factories, components and the like (except for run and config blocks). It will then do the same for the module being loaded.

After each of those injectables are instantiated, the config and run blocks are executed.

Issues With Dynamically Loading Modules

Due to the way the above process occurs, module definitions are not re-evaluated after a bootstrapping has occurred. The trick to getting it to work is to perform the initialization described above, without having to rebootstrap the application (thus losing the application’s state).

Another issue with dynamic modules is the fact that dependencies developed against Angular 1.x don’t have to take this type of behavior into consideration.

Take UI-Router, for example. It assumes that all states are declared at bootstrap time. If a URL is routed to, or a $state.go is performed, it can simply throw an error saying the state wasn’t found.

If that state actually did exist, but in an uninitialized module, it would have no knowledge of that fact.

If dynamic modules are implemented, this is the type of problem you should be prepared for. On our application, we encountered two such dependencies that had to have special modifications done in order to support this feature.

The Solution

Ultimately, the solution was pretty simple. It consists of a single class that is all of 50 lines.

A few quick notes about the class:

  1. It is intended to be used as a singleton
  2. It requires all of the providers necessary to construct your app ` $injector, $compileProvider, $controllerProvider, $filterProvider, $provide ` These are used to build the injectables for the application.
  3. We use the ng-app element’s injector function. This is because the $injector passed in to the class is different than that of the app
  4. The root application is set before any subsequent modules are loaded
import Angular from 'Angular';

export default class DynamicAngularModuleLoader {
    constructor() {
        this._initializedModules = {};
    }
    // This is the only function that is intended to be called by the application. It will push it to the root module's
    // list of required modules (the requires property on the module).
    // It will then initialize all of the pieces of the module by calling _initializeModule.
    // After that is done, it will execute the configuration and run blocks associated with all of the newly defined modules.
    loadModule(moduleName) {
        this._rootApplicationModule.requires.push(moduleName);
        this._initializeModule(moduleName);
        this._executeRunsAndConfigs();
    }
    // This function will subsequently mark all of the modules that the root application has already initialized
    // (this is the module that was bootstrapped). It also keeps a reference to the Angular module to dynamically
    // push new dependency modules to.
    set rootApplicationModule(module) {
        this._markChildModulesInitialized(module);
        this._rootApplicationModule = Angular.module(module);
    }
    _executeRunsAndConfigs() {
        const $injector = Angular.element(document.querySelector('[ng-app]')).injector();
        this._configBlocks.forEach(this._executeInvocation.bind(this));
        this._runBlocks.forEach($injector.invoke);
        delete this._configBlocks;
        delete this._runBlocks;
    }
    // This is where the bulk of the work is being done. It will iterate over the _invokeQueue, which is the list of services,
    // factories, etc that are associated with a module.
    // It will also push configuration and run blocks onto a collection to be executed post-initialization.
    // It then marks it as an already initialized module. If this step is skipped, singletons like services are overwritten,
    // configuration blocks are executed multiple times, all of which leads to problems down the line.
    // Lastly, it will iterate over the list of dependency modules and initialize those, too.
    _initializeModule(moduleName) {
        const module = Angular.module(moduleName);
        module._invokeQueue.reverse().forEach(this._executeInvocation.bind(this));
        this._configBlocks = this._configBlocks ? this._configBlocks.concat(module._configBlocks) : module._configBlocks;
        this._runBlocks = this._runBlocks ? this._runBlocks.concat(module._runBlocks) : module._runBlocks;
        this._initializedModules[moduleName] = true;
        module.requires.forEach((nestedModule) => {
            if (!this._initializedModules[nestedModule]) {
                this._initializeModule(nestedModule);
            }
        });
    }
    // This function simply put, takes an entry from the _invokeQueue and initializes it by calling a specific provider's
    // method against a specific construct (like a service). These are specified in an array such as:
    // ['$compileProvider', 'component', ['componentName' ...]].
    _executeInvocation([providerName, providerMethod, construct]) {
        const provider = this.providers[providerName];
        provider[providerMethod].apply(provider, construct);
    }
    _markChildModulesInitialized(module) {
        if (!this._initializedModules[module]) {
            this._initializedModules[module] = true;
            const angularModule = Angular.module(module);
            angularModule.requires.forEach((key) => {
                this._markChildModulesInitialized(key);
            });
        }
    }
}

Implementation

return new Promise((resolve, reject) => {
    require.ensure(['dependent-module'], (require) => {
        const {DependentModule, DependentReducers} = require('dependent-module');
        try {
            dynamicAngularModuleLoader.loadModule(DependentModule);
            dynamicReducerLoader.injectReducer({
                dependent: combineReducers(DependentReducers)
            });
        } catch (err) {
            reject(err);
        }
        resolve();
    });
});

The above is a snippet of how we use the dynamic module class. As you can see, we use require.ensure, which is a feature of Webpack. It is extremely nice because it will automatically code split that module for us into its own bundle. This is where the majority of our performance gains came from.

We pull in the Angular module (DependencyModule), and perform the loadModule call. We also add the reducer function to our $ngRedux store.

Handling Routes & Other Dependencies

The biggest problem faced by this solution is when to load the new modules. We know that our parent application, the one controlling login and bootstrapping of the application must be loaded first. However, at login, we determine where to direct our users.

This is where it gets a bit tricky. We have to determine which module to load, based on a state to direct to, or a URL being refreshed.

We have strictly enforced standards around our naming conventions of routes and URLs. This allows us to use regex to determine the module to load. For instance, our route states have ‘index.subModule’ preceding the child state, so we know the module to load based on the index.(\w+) group. As an example, a link across modules would look like this:

<a ui-sref="index.subModule">Go To Sub-Module</a>

Using our regex, we are able to determine that we need to ensure that subModule has been loaded. If it has not, we need to perform the HTTP request for that portion of the script and then initialize the new module using the code above.

Our URLs are also preceded with what submodule it is a part of. This means /(\w+)/ can also be used to determine which submodule to load in the case where a direct URL is used (or a hard refresh, for example). It will grab the first word-character grouping of the relative URL, which will match our Angular module name of the sub-application being accessed.

In addition to our routes, we utilize Redux. Similar to UI-router, our library does not support dynamically registered Angular modules. We had to dig into our dependency’s code to determine how it works, and a solution to the problem we introduced. It’s an unfortunate side-effect, but we found it to be worth the extra effort, especially considering it only impacted two of our libraries.

Brittleness

Simply put, the above code is brittle, but functional. There are several things that could go wrong (like the introduction of another type of injectable), the Angular app is bootstrapped versus using ng-app, or I could have overlooked something entirely!

In addition, it’s quite possible that the Angular devs introduce a new process for module loading or bootstrapping, entirely. Angular 1.5 was intended to be a transition library to Angular 2. It’s quite possible that the core team decides to revamp the inner workings of the code base in order to make that transition even easier, but thus breaking this solution.

Introducing tried and tested libraries could also have inadvertent bugs due to the way modules are defined with this solution. It puts the developer introducing the library in a state where it could be the library, the way the library was implemented, or how modules are loaded by the application if this solution is implemented.

That’s all, folks!

By no means is this a guaranteed solution to the problem, but it’s a start. My hope is that this can be used as reference for those of us who are maintaining Angular 1.x apps and don’t have the luxury of swapping that dependency out. If you give this a try and find any issues, feel free to reach out to me on Twitter @angular_evan! I’d be interested to see if there are scenarios I did not consider.