Skip to main content
Ben Nadel at InVision In Real Life (IRL) 2018 (Hollywood, CA) with: Jonas Bučinskas
Ben Nadel at InVision In Real Life (IRL) 2018 (Hollywood, CA) with: Jonas Bučinskas

Preloading Lazy-Loaded Feature Modules In Angular 6.1.9

By
Published in Comments (6)

In the last couple of days, I've started to dig into the lazy-loading of feature modules in Angular 6. With lazy-loading, segments of the code-base get loaded on-demand as the user navigates to the relevant areas of the application. The Angular router, however, provides a configuration that can preload these lazy-loaded feature modules such that they don't impact your application's "time to first interaction"; but, in such a way that it allows the code to be made available within the application before the user even needs it. To explore this concept, I wanted to take my previous demo of the "loading indicator" for lazy-loading modules and add the PreloadAllModules router configuration.

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

Out of the box, Angular 6.1.9 provides two preloading strategies: NoPreloading, which is the default, and PreloadAllModules. By using the PreloadAllModules strategy, the Angular router will start making HTTP requests to load lazy-loaded feature modules as soon as the Angular application has been successfully bootstrapped. The hope is, of course, that any given lazy-loaded module is then already loaded by the time the user wants to engage with the associated area of the application.

NOTE: You can also define your own custom preloading strategies if you want more granular control over which modules gets loaded and when.

To use the PreloadAllModules strategy, all we have to do is import it and then provide it as part of the RouterModule.forRoot() service options in our Application module:

import { PreloadAllModules } from "@angular/router";
import { RouterModule } from "@angular/router";

// .... [truncated]

RouterModule.forRoot(
	// ....
	{
		// Tell the router to use the HashLocationStrategy.
		useHash: true,
		enableTracing: false,

		// This will tell Angular to preload the lazy-loaded routes after the
		// application has been bootstrapped. This will extend to both top-level
		// and nested lazy-loaded modules.
		preloadingStrategy: PreloadAllModules
	}
)

That's literally all there is to it! At this point, any lazy-loaded feature modules will get preloaded in the Angular application shortly after the application becomes responsive.

In my last post, I talked about how the Router emits events related to lazy-loaded modules:

  • RouteConfigLoadStart
  • RouteConfigLoadEnd

In a preloading scenario, these events still fire. The only difference is that they are not necessarily tied to an active user navigation request. As such, these events may fire during the preload; or, they may fire during the navigation to a lazy-loaded feature that has not yet been loaded into the application.

Because of this new timing characteristic, I wanted to take my previous "loading indicator" demo and update it such that it didn't show the "loading module" indicator during the preloading process on a slow network connection. To do this, I had to update my logic to take active navigation into account. Now, instead of just monitoring the config events, I'm also monitoring the navigation Start and End (and Error and Cancel) events as well:

// Import the core angular services.
import { Component } from "@angular/core";
import { Event as RouterEvent } from "@angular/router";
import { NavigationCancel } from "@angular/router";
import { NavigationEnd } from "@angular/router";
import { NavigationError } from "@angular/router";
import { NavigationStart } from "@angular/router";
import { Router } from "@angular/router";
import { RouteConfigLoadEnd } from "@angular/router";
import { RouteConfigLoadStart } from "@angular/router";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

@Component({
	selector: "my-app",
	styleUrls: [ "./app-view.component.less" ],
	templateUrl: "./app-view.component.htm"
})
export class AppViewComponent {

	public isShowingRouteLoadIndicator: boolean;

	// I initialize the app view component.
	constructor( router: Router ) {

		this.isShowingRouteLoadIndicator = false;

		// *************************************************************************** //
		// CAUTION: Even though we are preloading our lazy-loading modules with the
		// "PreloadAllModules" configuration, the following Router events still fire
		// when the RouterPreloader's requests for the remote code are initiated and
		// completed, respectively. As such, this code is still relevant. And, on a
		// slower connection, may still show the loading indicator shortly after the
		// application has been bootstrapped (depending on what the initial URL of
		// the application is).
		// *************************************************************************** //

		// As the router loads modules asynchronously (via loadChildren), we're going to
		// keep track of how many asynchronous requests are currently active. If there is
		// at least one pending load request, we'll show the indicator.
		var asyncLoadCount = 0;

		// As the user navigates around the application, we're going to keep track of how
		// many pending navigation requests are currently active. This way, we can know
		// if the asynchronous module loading is [possibly] happening because of a user
		// navigation; or, if it's happening as part of the pre-loading.
		var navigationCount = 0;

		// The Router emits special events for "loadChildren" configuration loading. We
		// just need to listen for the Start and End events, amidst the rest of the
		// events, in order to determine if we have any pending configuration requests.
		router.events.subscribe(
			( event: RouterEvent ) : void => {

				if ( event instanceof RouteConfigLoadStart ) {

					asyncLoadCount++;

				} else if ( event instanceof RouteConfigLoadEnd ) {

					asyncLoadCount--;

				} else if ( event instanceof NavigationStart ) {

					navigationCount++;

				} else if (
					( event instanceof NavigationEnd ) ||
					( event instanceof NavigationError ) ||
					( event instanceof NavigationCancel )
					) {

					navigationCount--;

				}

				// If there is at least one pending asynchronous config load request AND
				// it is taking place while a the user is actively navigating around the
				// application, then let's show the loading indicator. This way, we don't
				// show the loading indicator during the preloading of lazy modules. This
				// isn't an exact science, since the navigation may not be tied to the
				// config load request. But, the small delay in rendering the indicator
				// should make the fuzzy-association a non-issue (as unrelated navigation
				// events will start and end almost instantly).
				// --
				// CAUTION: I'm using CSS to include a small delay such that this loading
				// indicator won't be seen by people with sufficiently fast connections.
				this.isShowingRouteLoadIndicator = !! ( navigationCount && asyncLoadCount );

			}
		);

	}

}

As you can see, I am keeping two counters: one for pending config requests and one for pending navigation requests. If the config requests are present, but there is no active navigation, I am assuming that the config requests are being made as part of the preloading of lazy-loading modules. As such, I don't show the loading indicator.

However, if both a config request is pending and an active navigation request is in play, then the chances are good that the lazy-loading module is being loaded because of said navigation. In such a scenario, I do want to show the "loading" indicator to the user in order to provide feedback with regard to the application's responsiveness

There is a chance the the navigation is unrelated to the preloading of Angular modules. However, since I am providing a slight delay in the actual rendering of the indicator (in the CSS), the navigation state should change before the loading indicator is flashed on the screen.

To refamiliarize you the demo, here is the HTML view associated with the above component:

App View

<p>
	<a routerLink="/app/">Home</a> &mdash;
	<a routerLink="/app/feature-a">Feature A</a> &mdash;
	<a routerLink="/app/feature-b">Feature B</a> &mdash;
	<a routerLink="/app/feature-c">Feature C</a> &mdash;
	<a [routerLink]="[ '/app', { outlets: { aside: 'aside' } } ]">Aside</a>
</p>

<router-outlet></router-outlet>

<router-outlet name="aside"></router-outlet>

<!-- I indicate that a router module is being loaded asynchronously. -->
<div
	*ngIf="isShowingRouteLoadIndicator"
	class="router-load-indicator">
	Loading Module
</div>

And, here is the LESS / CSS that styles this view:

:host {
	display: block ;
}

a {
	color: red ;
	cursor: pointer ;
	text-decoration: underline ;
}

// On a fast network connection, the lazy-loaded modules will load almost instantly. In
// such a case, we don't want to bother showing the asynchronous loading indicator as it
// will simply flash the UI and create a distracting user experience. As such, we want to
// put a small delay on the "observability" of the loading indicator. This way, on a fast
// connection, it will be removed before the delay is consumed; and, on a slower network
// connection, it will be shown to the user soon after the asynchronous load starts.
// --
// CAUTION: keyframes are not "protected" by Angular's simulated encapsulation. As such,
// this animation name needs to be universally unique to the application.
@keyframes router-load-indicator-animation {
	from {
		opacity: 0.0 ;
	}

	to {
		opacity: 1.0 ;
	}
}

.router-load-indicator {
	// Delay the animation for a fast network connection (so users don't see the loader).
	animation-delay: 100ms ;
	animation-duration: 200ms ;
	animation-fill-mode: both ;
	animation-name: router-load-indicator-animation ;
	// --
	background-color: #ffdc73 ;
	border-radius: 5px 5px 5px 5px ;
	box-shadow: 0px 2px 2px fade( #000000, 20% ) ;
	color: #000000 ;
	font-family: monospace ;
	font-size: 16px ;
	left: 50% ;
	padding: 7px 15px 7px 15px ;
	position: fixed ;
	text-transform: lowercase ;
	top: 10px ;
	transform: translateX( -50% ) ;
	z-index: 2 ;
}

As you can see, even when the [ngIf] template directive renders the loading indicator in the application, the CSS animation provides 100ms delay before it actually displays to the user. This delay balances-out the fuzzy association of the navigation events to the remote configuration loading events.

Now, if I switch my Chrome Developer network tools to slow a connection and then load the root of the application, we get the following browser output:

Lazy loaded feature modules can be preloaded right after bootstrapping in an Angular 6.1.9 application.

As you can see, the lazy-loaded feature modules are preloaded into the application right after the application is bootstrapped. And, since there is no active navigation, our counter-logic prevents the loading indicator from being shown.

But, that's because we loaded the root of the application. What happens if the initial request to the application is for a lazy-loaded module? In that case, there will be an active navigation while the lazy-loaded modules are being fetched:

Preloading modules still provides hooks to show a loading indicator via router events in Angular 6.1.9.

As you can see, if the user's initial request is to a lazy-loaded feature module within the application, then the Angular router will perform an active navigation after the application has been bootstrapped. As such, the loading indicator will be shown to the user, as expected and desired.

For reference, here is my full App module code:

// Import the core angular services.
// import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";
import { PreloadAllModules } from "@angular/router";
import { RouterModule } from "@angular/router";
import { Routes } from "@angular/router";

// Import the application components and services.
import { AppViewComponent } from "./views/app-view.component";
import { AsideView } from "./views/aside-view/aside-view.module";
import { FeatureAView } from "./views/feature-a-view/feature-a-view.module";
import { FeatureBView } from "./views/feature-b-view/feature-b-view.module";
import { FeatureCView } from "./views/feature-c-view/feature-c-view.module";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

// When we included a routable view into the router tree, we have to define the routes
// and, SOMETIMES, import the view modules (when statically loaded). In order to keep
// the routing semantics consistent across our views, I'm pushing both the ROUTE and
// MODULE definitions into the subview. This way, the parent context always SPREADS both
// the modules (into the imports) and the routes (into the RouterModule) into its own
// definition. This allows a module to switch from statically loaded to lazy loaded
// without the parent context having to know about it.
export interface RoutableView {
	modules: any[],
	routes: Routes
}

@NgModule({
	imports: [
		BrowserModule,
		// NOTE: When a routing module is statically included, then the routing module
		// needs to be explicitly imported. In order to not worry about this divergence,
		// let's let the child module define the importable modules (which may or may
		// not be an EMPTY ARRAY - empty if lazy-loaded).
		// --
		...AsideView.modules, // <--- empty array.
		...FeatureAView.modules, // <--- empty array.
		...FeatureBView.modules, // <--- empty array.
		...FeatureCView.modules,
		// --
		RouterModule.forRoot(
			[
				{
					path: "app",
					children: [
						// CAUTION: These routes define LAZY LOADED modules.
						...FeatureAView.routes, // <--- using "loadChildren"
						...FeatureBView.routes, // <--- using "loadChildren"
						...AsideView.routes, // <--- using "loadChildren"

						// CAUTION: These routes define STATICALLY LOADED modules.
						...FeatureCView.routes
					]
				},
				// Handle root redirect to app.
				{
					path: "",
					pathMatch: "full",
					redirectTo: "app"
				},
				// Handle root not-found redirect.
				{
					path: "**",
					redirectTo: "/app"
				}
			],
			{
				// Tell the router to use the HashLocationStrategy.
				useHash: true,
				enableTracing: false,

				// This will tell Angular to preload the lazy-loaded routes after the
				// application has been bootstrapped. This will extend to both top-level
				// and nested lazy-loaded modules.
				preloadingStrategy: PreloadAllModules
			}
		)
	],
	providers: [
		// CAUTION: We don't need to specify the LocationStrategy because we are setting
		// the "useHash" property in the Router module above (which will be setting the
		// strategy for us).
		// --
		// {
		// provide: LocationStrategy,
		// useClass: HashLocationStrategy
		// }
	],
	declarations: [
		AppViewComponent
	],
	bootstrap: [
		AppViewComponent
	]
})
export class AppModule {
	// ...
}

For the rest of the code, which is less relevant to the focus of this app, you can use the GitHub links above.

I know that the Angular router has some quirks; but, I've been a huge fan of what they are trying to do, especially with secondary routes. Now, seeing how easy it is to preload lazy-loaded feature modules in an Angular 6.1.9 application just makes me all the more excited.

Want to use code from this post? Check out the license.

Reader Comments

1 Comments

Hi, is it possible to detect target route outlet? I'm using nested lazy loaded modules and I'm facing problem that I can't distinguish root route change and child route change when using router.events.subscribe to be able to detect route change level.

15,841 Comments

@Michael,

Interesting question. Most of the time, I'm only trying to observe router-changes in the context of a particular router-outlet (such as when listening for param changes to reload local data). What are you trying to achieve? If I know your desired outcome, maybe I can think more clearly about a possible solution?

2 Comments

Great article!!!

I have a few question though. I created an HTTP interceptor, which shows and hides spinner when a HTTP request starts or finishes. Is there any way one can combine the logics you wrote with the one I wrote? I tried them separately, but they collide sometimes, and the spinner doesn't hide when it should.

2 Comments

I found the reason why the spinner doesn't hide sometimes. RouteConfigLoadEnd doesn't get triggered in case of auth guard errors.

Anyway, the idea of creating a logics of showing spinner for both HTTP requests and router events combined would be nice one to produce.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel