Custom Preloading and Lazy Loading Strategies with Angular
Angular has many features that allow us to configure apps to be as fast and high performing as possible. One of the critical features that enable responsive, quick Angular apps is the ability to lazy load code with the Angular Router. Lazy loading allows our initial bundles to remain small ensuring faster downloads and startup times for our app.
The Angular router has the fantastic ability to load code on demand as the user routes between views on our apps. By splitting our features into stand alone Angular Modules we can lazy load the module when the user clicks the link to navigate to this feature.
While this is powerful, we can further optimize this. Built into Angular, we can use its preload strategy. Utilizing this preload strategy, we can lazy load all modules in the background. This style of lazy loading keeps our main initial bundle small on first load and lazy loading all our modules in the background. This means that when the user clicks the link, the module has been preloaded already into memory. This mechanism allows Angular to immediately start rendering instead of waiting for the module to download over the network.
The preload strategy build into Angular looks something like the following in our app routing module:
@NgModule({
imports: [
RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
], // Define Preloading Strategies
exports: [RouterModule],
providers: [AppCustomPreloader]
})
export class AppRoutingModule {}
This pattern is great for performance and lazy loading progressively, but we can do better.
Custom Preload Strategies
With the Angular Router, we get the PreloadAllModules
strategy for free. This works well, but if our app is very large preloading, every module in the background may cause unnecessary data to be loaded in the background. Ideally, we would like to preload the core features or most common user paths in our app. This will allow core features to render immediately when the user navigates to the feature while keeping our core bundle small. As well as we continue to lazy load the rest of the less used features on demand when the user clicks the link.
Let's take a use case example app. In my app, I have three features. The first feature is my landing page/core home feature. We will call this feature-1
. My second feature is an often used feature in my app so I would like to preload just this feature. We will call this feature feature-2
. My last feature is rarely used so I would like to load this only if the user navigates to it. We will call this feature feature-3
.
// App Features
// feature-1: load with main core bundle
// feature-2: preload in background to be ready to use when user navigates to feature-2
// feature-3: only lazy load if the user navigates to feature-3
Without app set up, we now want to define our custom route preload strategy. Since we want to mark specific routes to be preloaded and others loaded on demand we need a way to describe this information. In our example below we use the data property on our route configs to define when our route should be preloaded.
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { AppCustomPreloader } from './app-routing-loader';
import { Feature1Component } from './feature-1/feature-1.component';
const routes: Routes = [
{
path: '',
redirectTo: 'feature-1',
pathMatch: 'full'
},
{
path: 'feature-1',
component: Feature1Component
},
{
path: 'feature-2',
loadChildren: './feature-2/feature-2.module#Feature2Module',
data: { preload: true } // Custom property we will use to track what route to be preloaded
},
{
path: 'feature-3',
loadChildren: './feature-3/feature-3.module#Feature3Module'
}
];
@NgModule({
imports: [
RouterModule.forRoot(routes, { preloadingStrategy: AppCustomPreloader })
], // Using our own custom preloader
exports: [RouterModule],
providers: [AppCustomPreloader]
})
export class AppRoutingModule {}
On our routes, we have an additional data
property defined with an object containing a preload: true
value. The data
property is reserved by the router to allow developers to add their custom values to their route configs. We will use this property to mark what routes should be preloaded based on our custom config.
In the example above we also have changed our preloading strategy to RouterModule.forRoot(routes, { preloadingStrategy: AppCustomPreloader })
. Now instead of using Angular's built-in preload all strategy, we are using our own. Let's take a look at the AppCustomPreloader
file.
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of } from 'rxjs';
export class AppCustomPreloader implements PreloadingStrategy {
preload(route: Route, load: Function): Observable<any> {
return route.data && route.data.preload ? load() : of(null);
}
}
Above is our custom reloader strategy. We implement the PreloadingStrategy
interface provided by Angular. A preloading strategy expects a class with a method preload()
. The preload method should return an Observable calling the load parameter or returning an Observable of null. In the preload
method we can determine the logic of if we should preload the module or not. The preload
method receives the active route as a parameter. With this route, we can look at the data property we set and determine if the preload flag was set and load the module accordingly.
Running Example
Now that we have the custom preload strategy implemented let's take a look at the running example. Our Angular CLI app has implemented the three features that we defined above. Using the Chrome Developer Tools, we can inspect them Network traffic and see how our code is loaded.
If we look at the network tools above, we can see our main application code bundled and loaded by the browser. Towards the end of the network requests, we see a 1.7...chunk.js
file. This file is our custom eagerly loaded feature two that was loaded asynchronously after our application bootstrapped. To prove this is the case if we navigate to feature we will see no additional requests in the network as it was already preloaded.
Now if we navigate to feature 3, we will see a new no demand network request for the feature three bundle as soon as we click the link to feature 3.
With the Angular router and custom preload strategies, we can customize how we load our code to optimize best our applications use cases. Check out the Angular CLI demo of this project below.