Lazy loading Angular applications
Sébastien D.
Posted on March 28, 2021
In very rare circumstances, you might want to delay the loading of your Angular application. In this article, I'll show you how you can do it.
WARNING: Don't do this lightly. Carefully evaluate if you really need to do this, as it
can have a devastating effect on user experience!
Angular module import side-effects
In my previous article, I've briefly explained the Angular application bootstrap process. One thing that I mentioned there is that the import statements remain at runtime, and are taken care of by Webpack.
What I didn't mention though is what happens as soon as Webpack imports an Angular module; for instance with the following line:
import { AppModule } from './app/app.module';
When you see this line, you might think that nothing much happens, apart for the AppModule
to be loaded and available for use in the rest of the current module. Well actually there's a side-effect at play here!
As soon as Webpack loads an Angular module, the decorator attached to the class of the Angular module is executed. Let me explain through an example:
As you can see, this is the Angular 1-01 module. It's a simple class with a decorator containing metadata. But what you may not know is that decorators are not just metadata.
Decorators are actually functions that are attached to elements (e.g., classes, methods, accessors, etc). They receive the decorated element as argument, and can modify those at will. TypeScript/JavaScript decorators are in fact instances of the decorator design pattern.
But the interesting question here is really when that decorator function get executed! When attached to a class, decorators are executed as soon as the class declaration is executed. And since Angular module classes are usually declared at the top-level, the class declarations get executed as soon as the ES module is loaded by Webpack!
Thus, coming back to this line:
import { AppModule } from './app/app.module';
This is clearly not side-effect free code! As soon as the module is loaded, the class declaration of the module is executed, and the same goes for the associated decorator function! This is important to keep in mind; I'll come back to this in a second.
Problematic situation
Before I get to the "how", let me describe a situation where delaying the loading of an Angular application makes sense.
In the project that I'm currently working on, we use the Auth0 Angular SDK. That library takes care of the authentication process. In addition, it provides an Angular HTTP interceptor, which can be used to attach OAuth access tokens to relevant outgoing HTTP requests (e.g., back-end API calls).
For that HTTP interceptor to function, the AuthModule
of the SDK must be loaded, and configured:
AuthModule.forRoot({
domain: 'YOUR_AUTH0_DOMAIN',
clientId: 'YOUR_AUTH0_CLIENT_ID',
httpInterceptor: {
allowedList: [ ... ],
...
},
...
}),
So far, so good. Where's the problem you might ask? Well the allowedList
above is a list of URLs/URL patterns that the HTTP interceptor will use to determine if the access token should be attached to a request or not. In our application, we didn't want to simply hardcode that list, as it varies between environments. Before configuring the AuthModule
, we first needed to load the environment configuration file. The environment configuration file is a static JSON file that contains the configuration of the current environment.
Fortunately, the Auth0 Angular SDK provides a way to postpone the configuration of the module, using an APP_INITIALIZER
:
Great, problem solved... Or not?
Unfortunately, not in our case! Why? Because our application already has other app initializers, some of which requiring the injection of an HttpClient
instance. And this is where the out of the box solution failed us. As soon as the HttpClient
needs to be injected somewhere in the application, the Auth0 HTTP interceptor gets instantiated. And if at that point in time the Auth0 module has not been configured yet, then the interceptor crashes with an error explaining that the configuration is missing. Doh!
Classic chicken and egg problem!
Unfortunately for us, we couldn't easily get rid of the dependency on the HttpClient
in the other initializers; our only solution was to load the configuration even before the Angular application was started, and to delay the evaluation of the AppModule
decorator in order to be sure that our configuration was already loaded/available by the time it ran.
Why is that? Well because, as we've seen, the @NgModule
decorator on AppModule
gets executed as soon as the module is imported, and main.ts
imports it by default.
Alright, now let's look at how to delay the bootstrap of an Angular application.
Delaying the loading and execution of Angular
The key to delay the loading/execution of an Angular application is in the default entry point: main.ts
.
The idea is to postpone the moment where platformBrowserDynamic().bootstrapModule(...)
gets called. But as I've hinted before in this article, it is not enough. If we want to avoid the side-effects caused by the AppModule
import, we also need to get rid of that import statement.
But if we don't import the AppModule
, then how do to bootstrap it? Well luckily for us, Angular has support for lazy loading modules:
const routes: Routes = [
{
path: 'items',
loadChildren: () =>
import('./items/items.module').then((m) => m.ItemsModule),
},
];
Lazy loading Angular modules is done using dynamic imports. Such imports are only executed when needed.
We have all the pieces of the puzzle now:
- Remove the
AppModule
top-level import - Delay the call to
platformBrowserDynamic().bootstrapModule(...)
Let's see the solution now:
Let me explain how this works. First, as explained before, we don't import AppModule
. Second, we load the runtime configuration of our application using the runtimeConfigLoader$
observable. Once the configuration has been loaded (line 32+), we store the configuration in sessionStorage
-- it's an arbitrary choice; could've been localStorage
or other means instead.
Finally, we switch to a different observable using the following:
return from(import('./app/app.module')).pipe(
concatMap((mod) => {
platformBrowserDynamic().bootstrapModule(mod.AppModule);
return of(void 0);
})
);
The import
statement returns a Promise
, which provides us with the ES module. Once the ES module is available (line 49+), we finally use platformBrowserDynamic().bootstrapModule(...)
to load Angular and bootstrap the AppModule
.
And there you have it, lazy loading of an Angular application. Of course, the code above corresponds to a specific scenario, but the same approach can be used to load an Angular application on-demand.
Conclusion
In this article, I've explained that importing Angular modules has side-effects, and I've explained how to avoid those and how to lazily bootstrap an Angular application.
Keep in mind that this should be avoided, as it slows down the application startup, and can have a very negative impact on user experience.
That’s it for today!
PS: If you want to learn tons of other cool things about product/software/Web development, then check out the Dev Concepts series, subscribe to my newsletter, and come say hi on Twitter!
Posted on March 28, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
July 16, 2024