All Articles

Custom preloading strategy for Angular modules

Lets say we have a medium sized Angular application and each large feature split into a lazy loaded module.

When the application starts, we load only the main modules and all the routes are lazy loaded, including the first one that we navigate to:

export const routes: Routes = [
{ path: '', redirectTo: 'items', pathMatch: 'full' },
{
path: 'items',
loadChildren: 'app/+items/items.module#ItemsModule',
},
{
path: 'item',
loadChildren: 'app/+item-details/item-details.module#ItemDetailsModule',
},
{
path: 'admin',
loadChildren: 'app/+admin/admin.module#AdminModule',
},
];

So what happens is that the app, core and shared modules are loaded, right away we navigate to a feature page and the coresponding module is loaded. When we go to item details page, since the module is lazy loaded, before we do something we have to wait for it to get loaded.

This is already an awesome setup and thumbs up if you use it 👍

We can improve things a bit by using a preloading strategy that Angular provides for us. When we provide the routes to the router module we have a second argument where we can specify a strategy.

RouterModule.forRoot(routes, { preloadingStrategy: NoPreloading })

Out of the box Angular provides two strategies already implemented for us, pretty explanatory from the name:

  • NoPreloading — no modules are preloaded, this is the default behaviour

  • PreloadAllModules — all modules are preloaded as fast as possible

While this will work in some scenarios, we might want to create something a bit more complex here. All we have to do is create a class that implements PreloadingStrategy class.

A good strategy here would be to load quickly just what is required and load some of the other modules with a small delay. We might know that after the initial load most of the users will go to a specific feature module, then after everything is loaded we can preload that feature module where we think users might go, or maybe preload all the other modules if we don’t have that many.

We start by adding a data object to the routes config, so we can leverage this in our custom preloading strategy:

export const routes: Routes = [
{ path: '', redirectTo: 'items', pathMatch: 'full' },
{
path: 'items',
loadChildren: 'app/+items/items.module#ItemsModule',
data: { preload: true, delay: false },
},
{
path: 'item',
loadChildren: 'app/+item-details/item-details.module#ItemDetailsModule',
data: { preload: true, delay: true },
},
{
path: 'admin',
loadChildren: 'app/+admin/admin.module#AdminModule',
data: { preload: false, delay: true },
},
];

Inside the data object I’ve added two properties, both boolean:

  • preload — if we want to preload that module or not

  • delay — if we want to load it right away or with a delay

Then we implement the preload method and decide which modules we preload right away and which we load with a small delay:

export class AppPreloadingStrategy implements PreloadingStrategy {
preload(route: Route, load: Function): Observable<any> {
const loadRoute = (delay) => delay
? timer(150).pipe(flatMap(_ => load()))
: load();
return route.data && route.data.preload
? loadRoute(route.data.delay)
: of(null);
}
}

Normally we would just check to see if the route has the preload property set to true and then we would call the load function, if not we would return an observable with null value (this will indicate that we don’t want any preloading):

return route.data && route.data.preload ? load() : of(null);

This can be improved by creating a loadRoute function that takes an argument, the delay property. If delay is false we call the load function right away. If *delay is true *an observable that emits a value after an interval is created, with the timer method, and the result is flat mapped to calling the load method.

The only thing left is to use this new strategy:

RouterModule.forRoot(routes, {
   preloadingStrategy: AppPreloadingStrategy
})

Using this our module is already loaded when we need it and the user has an improved experience.