Angular Router: Declarative Lazy Loading - Angular 2

August 12, 2016 0 Comments angular 2, angular router

Angular Router: Declarative Lazy Loading - Angular 2

Lazy loading speeds up our application load time by splitting it into multiple bundles, and loading them on demand. We designed the router to make lazy loading simple and easy.

In this article we will use a mail app as an example. This app has two sections: messages and contacts. At launch, our application displays messages. Click the contacts button and it shows the contacts.

Let's start by sketching out our application.

The button showing the contacts UI can look like this:

In addition, we can also support linking to individual contacts, as follows:

In the code sample above all the routes and components are defined together, in the same file. This is done for the simplicity sake. How the components are arranged does not really matter, as long as after the compilation we will have a single bundle file 'main.bundle.js', which will contain the whole application.

Just One Problem

There is one problem with this setup: even though ContactsCmp and ContactCmp are not displayed on load, they are still bundled up with the main part of the application. As a result, the initial bundle is larger than it could have been.

Two extra components may not seem like a big deal, but in a real application the contacts module can include dozens or even hundreds of components, together with all the services and helper functions they need.

A better setup would be to extract the contacts-related code into a separate module and load it on-demand. Let's see how we can do that.

Lazy Loading

We start with extracting all the contacts-related components and routes into a separate file.

In Angular 2, an ng module is part of an application that can be bundled and loaded independently. So we have defined one in the code above.

Referring to Lazily-Loaded Module

Now, after extracting the contacts module, we need to update the main module to refer to the newly extracted one.

The loadChildren properly tells the router to fetch the 'contacts.bundle.js' when and only when the user navigates to 'contacts', then merge the two router configurations, and, finally, activate the needed components.

By doing so we split the single bundle into two.

The bootstrap loads just the main bundle. The router won't load the contacts bundle until it is needed.

Note that apart from the router configuration we don't have to change anything in the application after splitting it into multiple bundles: existing links and navigations are unchanged.

Deep Linking

But it gets better! The router also supports deep linking into lazily-loaded modules.

To see what I mean imagine that the contacts module lazy loads another one.

Imagine we have the following this link in the main section or our application.

When clicking on the link, the router will first fetch the contacts module, then the details module. After that it will merge all the configurations and instantiate the needed components. Once again, from the link's perspective it makes no difference how we bundle our application. It just works.

Sync Link Generation

The RouterLink directive does more than handle clicks. It also sets the <a> tag's href attribute, so the user can right-click and "Open link in a new tab".

For instance, the directive above will set the anchor's href attribute to '/contacts/13/detail;full=true'. And it will do it synchronously, without loading the configurations from the contacts or details bundles. Only when the user actually clicks on the link, the router will load all the needed configurations to perform the navigation.

Navigation is URL-Based

Deep linking into lazily-loaded modules and synchronous link generation are possible only because the router's navigation is URL-based. Because the router does not have the notion of router names, it does not have to use any configuration to generate links. What we pass to routerLink (e.g., ['/contacts', id, 'detail', {full: true}]) is just an array of URL segments. In other words, link generation is purely mechanical and application independent.

This is an important design decision we have made early one because we knew that lazy loading is a key use case for using the router.

Customizing Module Loader

The built-in application module loader uses SystemJS. But we can provide our own implementation of the loader as follows:

Finally, you don't have to use the loader at all. Instead, you can provide a callback the route will use to fetch the module.

Preloading Modules

By default, the Angular router only loads modules when requested, but we might want to preload some modules so they are ready to go.

It isn't hard to write a service that examines the router configuration and selectively downloads modules in the background. Then the router can pull them from cache and navigate instantly.

Learn More

If you want to learn more about the Angular router, check out the book I've been writing on the subject:

It goes far beyond a "getting started" tutorial and explores the library in depth, including the mental model, design constraints, subtleties of the API, and more. It reveals deep insights into why the router works the way it does.

Angular Router: Declarative Lazy Loading - Angular 2

Tag cloud