What stopped us from doing this up until now?

This next section goes pretty deep into how lazy-loading works in Angular right now, and how it’s going to work in Angular in the future! Most of this is going away, but it’s still pretty interesting!

Lazy-loaded routes in Angular 7.x.x

We use RouterModule.forChild() and RouterModule.forRoot() to tell Angular about the route structure of our application. But how does it work? Let’s check out the Angular source and find out!

If we dig into the implementation of RouterModule.forChild() and RouterModule.forRoot() , we can see that when we pass in the array of routes, they are registered as a multi provider against the ROUTES InjectionToken :

The RouterModule (from the Angular 7.x.x source) takes the given route configuration and attaches them to the ROUTES InjectionToken .

This means that at runtime we’re going to have an injectable array of route configuration objects! But how does Angular use these ROUTES ? Again, let’s use the source:

The RouterConfigLoader (from the Angular 7.x.x source) is responsible for figuring out how to load a module based on its config.

The ROUTES are injected into the application’s Router when it is created. When Angular encounters a route with a loadChildren property on it, it uses the RouterConfigLoader to try and figure out how to do that loading. We can see that the RouterConfigLoader does something differently based on if typeof loadChildren is a string or not… but doesn’t loadChildren have to be a string?

Let’s have a look at the LoadChildren type:

TypeScript types for LoadChildren, showing that you can use either a string or an async function.

Isn’t that interesting! Even in a pre-Ivy world, loadChildren can be a string or an async function ! So that should mean that our fancy import() syntax will already work? Let’s try it out:

StackBlitz showing the alternative loadChildren syntax working in Angular 7.x.x

What? It does work! But how does this work?! Why have we been using the magic string syntax all along?!?!

The answer is there’s a catch… 🎣

Lazy-loaded routes in Angular 7.x.x with Ahead of Time compilation

If we were to take our above application and build it with the prod flag ( ng build --prod ), everything appears to work! But when we try to navigate to our lazy-loaded route, we get a big red error:

When running our app in AOT mode, we get an error: Runtime compiler is not loaded

This error makes sense! We used the --prod flag to enable the “Ahead-of-time” (AOT) compiler, which means we opted out of the “Just-in-time” (JIT) runtime compiler. If we look at where the error comes from, we can see it’s caused by the call to compileModuleAsync() in the RouterConfigLoader :

RouterConfigLoader#loadModuleFactory throws in AOT mode because it doesn’t have access to the JIT compiler at run time.

We end up down that else path because the instanceof check fails! When we use the import() operator with AOT, the object that we import from the lazy-loaded module is an NgModule instead of a NgModuleFactory . So how do we make sure that we are loading an NgModuleFactory ?

From NgModule to NgModuleFactory with the AOT Compiler:

The Angular compiler’s job is to statically analyse all of the code in our entire application, and to efficiently compile all of our templates and styles. It takes our NgModule files, and turns them into NgModuleFactory files, which contain the generated code that will create our views at runtime.

The compiler is able to start at a given file, and navigate through all of the import statements (e.g. import { Thing } from './path/to/thing'; ) and build up a tree of all of the referenced modules. In order to split our application into chunks, we have to change our code to explicitly break this tree of references apart, while also making sure that the compiler knows about all the split parts of our application. The way we do this in an Angular application is with the loadChildren property, specifically with the magic string format:

Angular AOT compiler code for finding lazy-loaded routes with the ROUTES InjectionToken

The Angular AOT compiler finds all the ROUTES by using the InjectionToken and then looks for any strings using the ./path/to/my.module#MyModule format. Each time it finds one, the compiler will start from the given path, build up the tree of referenced files, and compile each NgModule into an NgModuleFactory . If we don’t use that format, we don’t end up with the NgModuleFactory that the runtime needs. If we do use that format, then we end up with a generated file with an unknown path containing the NgModuleFactory , which means we can’t reference it with import() …

Altogether, this means that even though the types in Angular 7.x.x allow us to specify an async function for loadChildren it will never work in a production build of our application 😭😭😭. But why does the import() operator work in JIT mode?

The import() operator is another way to declare that we want to lazily reference another part of our application. Modern tooling can detect it, mark the referenced path as another entry point, and lazily load the reference at runtime. Unfortunately, only the Angular CLI knows how to turn a NgModule into an NgModuleFactory , and it doesn’t know about import() . We saw it working because JIT mode only needs an uncompiled NgModule .