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
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.
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:
- It is intended to be used as a singleton
- 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.
- 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
- The root application is set before any subsequent modules are loaded
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
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:
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.
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.