Angular NgModule Intro - Ahead Of Time Compilation And Lazy Loading - Avoid Common Pitfalls

In this post, we are going to do an introduction to Angular Modularity (the NgModule functionality) and understand why it enables several important features like ahead of time compilation and lazy loading. We will cover the following topics:

  • What is an Angular Module?
  • Angular Modules vs ES6 modules
  • What is a Root Module?
  • Making modules more readable using the spread operator
  • Angular Modules and Visibility
  • Angular Modules and Dependency Injection - Avoid surprises
  • Dynamic bootstrapping and Just In Time compilation
  • The Angular Ahead Of Time compiler in action
  • Static bootstrapping
  • Feature Modules
  • Angular Modules And The Router
  • Lazy Loading a Module using the Router
  • Shared Modules and Lazy Loading
  • Conclusions and Pitfall Summary

What is an Angular Module?

The name Module can sometimes be an overloaded term in programming, but the use of the name "Module" for this new functionality is actually consistent with previous AngularJs terminology.

Angular Modules are a close Angular counterpart of AngularJs modules, so using the same term really helps us make the transition smoother.

So what are Angular modules? Let's start by having a look at the newest official docs:

Angular modules consolidate components, directives, and pipes into cohesive blocks of functionality... Modules can also add services

So we can see here that an Angular module is used to group an inter-related set of Angular primitives.

Examples of modules

A good example of a module is the reactive form directives and services: these are directives that are aware of each other and are very interrelated.

There are also injectable services like the FormBuilder, which is functionally linked to those directives.

Another example of a module is the router directives and services, they are tightly linked and form a consistent unit.

But there could also be application-level modules: imagine an app that is divided into two sets of completely separate screens; we should probably separate them into two different modules.

What does an Angular Module look like?

This is an example of an Angular module, it defines the application module that we are about to use in our examples:

We can see here several things going on:

  • the @NgModule annotation is what actually defines the module
  • we can list the components, directives, and pipes that are part of the module in the declarations array
  • we can import other modules by listing them in the imports array
  • we can list the services that are part of the module in the providers array but read further on why this should only be used in some cases

This declarative grouping is useful if for nothing else for organizing our view of the application and documenting which functionality is closely related.

But Angular Modules are more than just documentation, what does Angular exactly do with this information?

Why are Angular modules needed?

An Angular module allows Angular to define a context for compiling templates. For example, when Angular is parsing HTML templates, it's looking for a certain list of components, directives and pipes.

Each HTML tag is compared to that list to see if a component should be applied on top of it, the same goes for each attribute. The problem is: how does Angular know which components, directives and pipes should it be looking for while parsing the HTML?

That is when Angular modules come in, they provide that exact information in a single place.

So in summary, we can say the following about Angular modules:

  • they are essential for template parsing, both in the Just In Time or Ahead Of Time Compilation scenarios as we will see
  • they are also very useful simply as documentation for grouping related functionality
  • They can be used to clarify which components and directives are meant to be used publicly vs internal implementation details, as we will soon see

Angular Modules vs ES6 modules

An Angular Module is something very different than an ES6 module: An ES6 is a formalization of the typical Javascript module that the community has been using for many years: wrap private details in a closure and expose only the public API we want.

An Angular Module is mainly a template compilation context but it also helps to define a public API for a subset of functionality as well as help with the dependency injection configuration of our application.

Angular Modules are actually one of the main enablers for fast and mobile-friendly applications, more on this further. Let's now go over the different types of modules and when they should be used.

What is a Root Module?

Each application only has one root module, and each component, directive and pipe should only be associated with a single module.

This is an example of an Application Root module:

Several things identify this as being a root module:

  • the root module has the conventional name of AppModule

  • the root module in the case of web applications imports the BrowserModule, which for example provides Browser specific renderers, and installs core directives like ngIf, ngFor, etc.

  • the bootstrap property is used, providing a list of components that should be used as bootstrap entry points for the application. There is usually only one element in this array: the root component of the application

We can see that this module can quickly grow to contain large arrays as the application grows. Before going further let's see how we can avoid this potential readability issue.

Making modules more readable using the spread operator

The simplest way to make a large module more readable is to define the lists of components, directives and pipes in external files. For example, we could define a couple of constant arrays in an external file:

We can then import these constants into the module definition, using the array spread ... operator:

We can see how this would make the module definition much more readable in the long term.

Angular Modules and Visibility

To understand how module visibility works in Angular, let's now define a separate module with only one component called Home:

Let's now try to use in our root module, by importing it:

You might be surprised to find out that this does not work. Even if you use the <home></home> component in your template, nothing will get rendered.

Why isn't the Home component visible?

It turns out that adding Home to the declarations of HomeModule does not automatically make the component visible to any other modules that might be importing it.

This is because the Home Component might be just an implementation detail of the module that we don't want to make publicly available.

To make it publicly available, we need to export it:

With this, the Home component would now be correctly rendered in any template that uses the home HTML tag.

Notice that we could also have only exported it without adding it to declarations. This would happen in the case where the component is not used internally inside the module.

Could we still import the component directly?

If we try to use a component directly that is not part of a module, we will get the following error:

Unhandled Promise rejection: Component Home is not part of any NgModule or the module has not been imported into your module.

This ensures that we only use components on our templates that have been declared as part of the public API of a given module.

Angular Modules and Dependency Injection

What about services and the providers property, when should we use it?

We might think that when importing a module, if that module has providers then only the component directives and pipes of that module would be able to see the service.

Let's see if that is true. Let's start by creating a simple service so we can give a concrete example:

Let's now add this service to the HomeModule:

This service is now as we would expect available in the Home component:

But the problem is that a new module will not create its own separate dependency injection context! The lessons service will actually be added to the global dependency injection context.

This means that LessonsService is available for injection anywhere in the application, including:

  • the root component
  • any component of the HomeModule
  • any component of any other module in general

But the Angular Dependency Injection container is hierarchical, meaning that unlike in AngularJs we could have created a separate context. So why did that not happen?

Why isn't a separate DI context created by default?

This happens by design: Modules that are directly imported are usually meant to enrich the global application and the injectables received are in most of the cases meant as application-wide singletons.

The goal is usually not to create almost a small separate sub-application inside the main application.

So the behavior of not creating a nested DI context is meant to
help with the most common use case: importing application-wide singletons.

This helps prevent the following error situations:

  • we import a module and are trying to use its injectables but we start getting errors saying that the injectable is not available
  • we run into subtle bugs caused by the presence of multiple instances of an injectable

But what about lazy-loading?

One of the main issues in Angular 1 was that the dependency injection container was not hierarchical: everything was in a single big dependency injection bucket.

This meant that when we navigated around and lazy loaded parts of the app, we could accidentally overwrite services with the same name with newer versions. This meant that the app would have different behavior depending on the sequence of navigation actions, which could cause errors that are hard to reproduce.

This was the main reason why lazy loading in AngularJs was not supported directly at the level of the framework, although it was still doable with for example ocLazyLoad.

We will see in a moment how modules can help out also with Angular lazy-loading, but first let's see how to use the root module to bootstrap our application.

Dynamic bootstrapping and Just In Time compilation

Angular modules specify a template compilation context, but how can we pass it to the compiler to bootstrap our app? We have several options.

An option that is good for using in development is to ship the Angular compiler to the browser, and dynamically compile the application:

This will compile all templates and bootstrap the application. This will come at the expense of a much larger application bundle, but that is OK when the server is actually running on your own development machine.

Let's see what we could use as a viable production alternative.

The Angular Ahead Of Time compiler in action

A more interesting alternative is to use the module information to do ahead of time compilation. The angular-cli will in the future allow us to do this transparently. We can already have an idea of how this works if we install the angular template compiler manually by following the developer guide:

npm install @angular/compiler-cli --save

This will provide a command line executable that we can use to parse all the templates of our application. This executable is actually a drop-in replacement for the Typescript compiler itself.

How can we use the ngc compiler to compile templates?

This new executable named ngc will look for Angular component classes and it will output another Typescript file, the component factory. We can run the executable like this:

./node_modules/.bin/ngc -p ./src

This assumes that src contains the tsconfig.json of your application. In the case of our main.ts component, this would create a file next to it named main.ngfactory.ts, that might contain something similar to this:

The code is a bit surprising, but we can have an idea of what is going on:

  • the constructor was extended to receive some core injectables, like for example renderers
  • the renderer is then being used to manually output the HTML by creating DOM elements, text content etc.
  • This is actually what an Angular renderer looks like under the hood, and except for the names of the variables it's actually quite close to what we would have written by hand

But how can we use these factory classes to bootstrap our app?

Static bootstrapping

We can statically bootstrap an application by taking the plain ES5 Javascript output of the generated factory classes. Then we can use that output to bootstrap the application:

This will cause the application bundle to be much smaller, because all the template compilation was already done in a build step, using either ngc or calling its internals directly.

Again this will all be made transparent when using the CLI:

  • ng serve will serve the app in Just In Time Mode
  • ng build -prod && ng serve -prod will serve the app in Ahead Of Time mode

So we can see how the root module can be used to bootstrap an application in different ways: another possible way would be to bootstrap the app on the server for server-side rendering.

But what about lazy loading, how does that relate to modules? Let's first explore feature modules, as we need that concept to help understand lazy loading.

Feature Modules

The HomeModule that we have been building so far is actually the beginning of a feature module. A feature module is meant to extend the global application.

This is the current version of the HomeModule, and there is actually something wrong in this definition:

To see what the problem here is, let's try to use a core Angular directive in the Home component template, like for example ngStyle:

If we try to run this we will run into the following error message:

Unhandled Promise rejection: Template parse errors:
Can't bind to 'ngStyle' since it isn't a known property of 'h3'.

But ngStyle was already imported into the application via BrowserModule, which includes CommonModule where ngStyle is defined. So why does this work in the application component but not in the Home component?

This is because HomeModule did not itself import CommonModule where the core ngStyle directive is defined.

It's not because the application itself has imported a module that the module will be visible inside other modules imported or not by the application

Each module needs to define separately what it can 'see' in its context, so the solution here is to import the CommonModule:

What we end up having is a typical feature module: it imports CommonModule, and provides some related components and services.

Feature Modules and Lazy Loading

We might want to split our larger application into a set of feature modules, but at a given point the application might become so large that we might want to use lazy loading to split it into multiple separately loadable chunks, each one corresponding to a feature module.

But there we might run into an issue with the module injectables, let's see why.

Angular Modules And The Router

To understand the issue that might arise with modules and lazy loading, let's first add the router to the application and define a simple route:

What we have done here is we have defined a /home URL than when hit will cause the Home component to be displayed. The way that this works is that when we hit the /home URL the Home component is displayed in place of the <router-outlet> HTML tag.

Have a look at this router introduction post if you would like to learn more about the router. We can also see the following:

  • We have imported RouterModule, which contains the routing directives like routerLink
  • But did that import router services, like for example RouteSnapshot ? Actually no, we can see in the RouterModule definition that no providers are defined
  • It's the forRoot call that imports the services, and we are going to see what does this call mean exactly and learn how to use it in our own feature modules

Lazy Loading the Home module

We will now refactor this code in order to make the Home component and anything inside HomeModule to be lazy loaded. This means everything related to the HomeModule will only be loaded from the server if we hit the home link (see the App HTML template above) in the App component, but not on initial load.

The first thing that we need to do is to remove every mention of the Home component or the HomeModule from the App component and the main routing configuration:

We can see here that the App component no longer imports HomeModule, instead the routing config uses loadChildren to say that if /home or any other url starting with it gets hit, then the file home.module should be loaded via an Ajax call.

Depending on your module loader configuration this will either load home.module.js or maybe home.module.ts if you are using in-browser transpilation.

What does a lazy loadable module look like

Let's have a look at an initial version of HomeModule, please beware that there is a new behavior introduced here different than a normal feature module:

With this small refactoring, we now have a fully working lazy-loadable module! We can see here that several things are going on:

  • the HomeModule defines its own routing configuration, which will be added to the main config and made relative to the /home path
  • The HomeModule is exported with the default keyword: this is essential otherwise the router will not be able to know which export to import from this file because there is no information about the name of the needed export; the router only knows the name of the module file
  • The routing configuration of HomeModule is added via a call to forChild, we will understand exactly what that is and why it's needed

What is different towards the import of a non lazy-loaded feature module?

Do you remember the problem mentioned earlier with AngularJs, that essentially lazy loading could trigger certain bugs that are hard to reproduce due to accidental overwrites of injectables?

in order to avoid this what Angular will do when lazy loading a module is that it will create a child dependency injection context. This Home DI context will contain the LessonsService, but this service will not be visible to the rest of the application.

The service will be visible to the Homecomponent, so if we try to inject it there it will work:

But if we now try to inject this into for example the main App component, we will get an error:

The error we will get is the following:

Error: Can't resolve all parameters for App

Multiple versions of the same injectable are possible

This is actually normal because LessonsService is lazy loaded. But what we could do is define a second LessonsService implementation, which has the same name for a different internal implementation:

We now just have to add this alternative definition of LessonsService which is defined in the other-lessons.service.ts file and add it to our AppModule providers configuration:

This would now work: The App component and the Home component would get injected different versions of LessonsService.

This also solves the structural problem that AngularJs had with lazy-loading: the hierarchical injector and the fact that the router creates a child DI scope for the lazy-loaded module elegantly enables reliable lazy-loading behavior

One final Pitfall with modules and lazy loading

We are close to knowing most of what we will need to know about modules, but there is one final notion that we need to introduce: Shared Modules, we will see how those relate to lazy loading and understand what are those forRoot and forChild calls.

Shared Modules

As our application grows, we can see that the need would arise of having a shared module which contains for example a set of commonly used services.

Take for example a AuthenticationService: you might want to use it at the level of the main module, but you might want to also reauthenticate inside feature modules, for example before doing an important operation like a financial transfer.

Let's now create a shared module, which contains the AuthenticationService, please note that this module definition would not work with lazy-loaded modules and we will in a moment see why:

We can now simply import this module anywhere we need it, for example at the level of the AppModule:

But now we would also want to use App inside HomeModule, and we would expect this to work:

This does not throw any error, the problem is that now we have two versions of AuthenticationService running in our application, because of the child DI context of the HomeModule:

  • one instance is created at startup and injected into App
  • the second instance is created when we click on the Home link which triggers the lazy loading of the HomeModule

And this is not the intended behavior, we again fall into a situation where we accidentally triggered a bug that might be very time consuming to troubleshoot.

We would like the service to be an application-level singleton. So how can we solve this? This is where the forRoot and forChild methods comes in.

Shared Modules and Lazy Loading

If we want to have a shared module that is correctly loaded by lazy-loaded modules, we can now conclude the following:

in such scenario the shared module cannot define services using the providers property

So we need another mechanism for a shared module to make its injectables available to both the root module and lazy loaded modules in a safe way. What we want is a way to do the following:

  • create an AuthenticationService instance when adding it to the root module
  • this instance will automatically by visible to any child DI contexts, like the HomeModule context
  • prevent the creation of a second instance of the service, by removing the providers declaration

By following some Angular conventions, we can do so by defining a forRoot method in SharedModule:

Notice that we removed AuthenticationService from the providers array. This means that when we import this module in HomeModule we will no longer create a duplicate service instance.

We can now use this method to import the service in the main module:

What this method does is that it returns the module plus providers that we need, but because the name of the method is forRoot, Angular will know how to use it: it will create the module context and the services declared in providers.

But when we import SharedModule into HomeModule without using forRoot, it will only process the module itself. SharedModule will have no information about the providers and so it will not instantiate a duplicate AuthenticationService.

Conclusions and Pitfall Summary

In this post we have learned why Angular Modules are necessary: they allow to group the functionality of our app into logical parts, and are essential for enabling ahead of time compilation (which means much better bootstrap performance) and lazy-loading.

We can now see how this feature was so important to have for the final release, without it some of the main benefits of the framework would not be available.

Modules are very useful, but beware of the following pitfalls:

  • do not redeclare a component, directive, etc. in more than one module
  • modules do not create their own DI context, so injectables are available also outside the module
  • unless the module is lazy loaded, in that case a separate DI context is created by the router to avoid accidental injectable overrides and prevent hard to troubleshoot bugs
  • if you have a shared module that needs to be added to a lazy loaded module, make sure that it does not have providers, because that would create duplicate service instances (unless that is the intended behavior)

The new modularity functionality is the enabler of some tremendous features: we can now have mobile friendly applications that load very quickly with only a couple tens of KB of Javascript but with tons of functionality.

We will be able to focus a lot more on application features, and with minimal configuration and in a very transparent way the Angular modularity features will make building blazing fast progressive web apps a breeze in the very near future.

Other posts on Angular

If you enjoyed this post, please have also a look at other popular posts on my blog that you might find interesting:

The Angular University On YouTube

Do you like to learn on YouTube ? Have a look at this one hour sample of the Complete Angular With Typescript Course, or Subscribe to my channel for more videos like this one: