DEV Community

Cover image for Optimize your Angular app's user experience with preloading strategies
Pierre Bouillon for This is Angular

Posted on • Updated on

Optimize your Angular app's user experience with preloading strategies

Performance optimization is a critical aspect of creating a successful Angular app. Slow loading times can lead to a frustrating user experience and ultimately drive users away. One common technique to improve performance is lazy loading, which allows the app to be broken down into smaller chunks that can be loaded on demand.

However, another important concept that is often overlooked is preloading strategies.

In this first article of the series "Load Less, Do More: A Guide to Angular Preloading Strategies", we will explore the world of preloading strategies and how they can be used to speed up the loading process and provide a smoother user experience. We will discuss what preloading strategies are, how they work, and the benefits they offer. To help illustrate these concepts, we will walk through an example of how to create a custom preloading strategy. By the end of this article, you will have a solid understanding of preloading strategies and how they can be applied to your Angular app.

Table of Contents

  1. Why performance optimization is important
  2. Why do preloading strategies matter?
  3. Default preloading strategies in Angular
  4. Using preloading strategies
  5. Implementing our own preloading strategy
  6. Takeaways

Why performance optimization is important

When creating web pages or online tools, performance optimization is a crucial aspect to provide a decent user experience, often referred to as "UX".

Do you remember the last time you were waiting for a web page to load? No matter what the site might have been, I doubt that you are remembering it as a pleasant experience.

In general, nobody likes to use a slow website as it can lead to frustration or even fear from the users browsing it ("Will it correctly saves my progress?", "Can I trust this website with my personal information?").

Ensuring that a website runs smoothly and loads quickly leads to a better UX overall.

Moreover, the speed of the app can impact how users perceive your brand: a slow and unresponsive site can create a negative impression, making users less likely to return.

Why do preloading strategies matter?

One of the first thing you fill hear about when seeking to improve the performances of your Angular app is lazy loading.

By splitting your application modules features into smaller chunks, you can load them on the fly instead of bundling them all together and force the user to download the whole website when accessing any page.

However, sometimes, you might know that, from a business perspective, a user will use some lazy modules when accessing a page. In that case, preloading them might make sense, to avoid an unnecessary latency to the user that will eventually want to see the page.

For those specific cases, Angular introduced the concept of preloading strategies.

A preloading strategy is a way to tell the Angular's router to load modules in the background, so that when the user navigates to a new page, the required modules have already been loaded and the page appears almost instantaneously.

A preloading strategy is a simple Angular class that extends the PreloadingStrategy abstract class defined as such:

abstract class PreloadingStrategy {
  abstract preload(route: Route, fn: () => Observable<any>): Observable<any>
} 
Enter fullscreen mode Exit fullscreen mode

When executed, this methods will return the call to fn if the route should be preloaded, or an Observable<null> if not.

Default preloading strategies in Angular

NoPreloading

By default, when using lazy loading, all modules are lazily loaded.

This is achieve by enabling the NoPreloading Strategy by default, which is described as:

Provides a preloading strategy that does not preload any modules.

Its usage will result in the loading of the targeted module only when requested:

NoPreloading

Unfortunately, this can results in slower subsequent loading time as the user navigates through the app.

PreloadAllModules

It's opposite also exists as the PreloadAllModules Strategy which will, as its name suggests it, preload everything:

Provides a preloading strategy that preloads all modules as quickly as possible.

Unlike the previous strategy, this time everything is loaded at once:

PreloadAllModules

However, neither of those strategy might prove optimal if you want finer control on the way you load your Angular app.

For those cases, it is possible to create and use your own preloading strategy.

Using Preloading Strategies

In order to indicate which preloading strategy the router should apply, you need to provide it to the preloadingStrategy property of the RouterModule.forRoot() method in your app's root module:

@NgModule({
  imports: [RouterModule.forRoot(routes, {
    preloadingStrategy: /* ... */
  })],
})
export class AppModule { }
Enter fullscreen mode Exit fullscreen mode

However, if you are now using Standalone Components, you might want to use a provider to call during the bootstrap of your application.

This can be done by adding withPreloading to the setup done in provideRouter:

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes, withPreloading(/* ... */))
  ],
});
Enter fullscreen mode Exit fullscreen mode

Note that if you write your own implementation of a preloading strategy, you will have to provide it on the root level too, so that it can be retrieved from the dependency injection container.

Implementing our own preloading strategy

We mentioned the importance of creating a custom preloading strategy sometime, let's see how to do it.

Purpose

Our custom preloading strategy will simply preload any lazy loaded route that has the flag preload set to true in its route data.

Setup

I will slightly modify the routes that I defined in the previous GIFs to add the flag:

export const routes: Route[] = [
  {
    path: "feature-1",
    loadComponent: () =>
      import("./app/feature-1/feature-1.component").then(
        (m) => m.Feature1Component
      ),
    // ๐Ÿ‘‡ This route should be preloaded
    data: { preload: true },
  },
  {
    path: "feature-2",
    loadChildren: () =>
      import("./app/feature-2/feature-2.route").then(
        (m) => m.routes
      ),
  },
];
Enter fullscreen mode Exit fullscreen mode

With our custom PreloadingStrategy, we should see that the "Feature #1" is loaded from the start but "Feature #2" is not.

Show me the code!

A preloading strategy is nothing more than a service that extends PreloadingStrategy.

Knowing this, let's create our FlagBasedPreloadingStrategy service:

// flag-based.preloading-strategy.ts
import { Injectable } from "@angular/core";
import { PreloadingStrategy, Route } from "@angular/router";

import { Observable } from "rxjs";

@Injectable({ providedIn: "root" })
export class FlagBasedPreloadingStrategy extends PreloadingStrategy {
  // ๐Ÿ‘‡ For clarity, I prefer `load` rather than `fn` for the callback name
  preload(route: Route, load: () => Observable<any>): Observable<any> {
    return route.data?.["preload"] === true ? load() : of(null);
  }
}
Enter fullscreen mode Exit fullscreen mode

Notice how we provided it in root so that we can indicate the router to use it on bootstrap

The logic doesn't matter much here. We simply check the current route for the flag and, if present, we will load the module and otherwise we will not.

Looking great! We still have to indicate the router to use it tho. This can be done in the same way as the built in ones: by providing it during the bootstrap phase:

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes, withPreloading(FlagBasedPreloadingStrategy))
  ],
});
Enter fullscreen mode Exit fullscreen mode

Now, if we open the network tab in the devtools, we should see that the feature #1 is loaded but feature #2 is not:

FlagBasedPreloadingStrategy 1

Similarly, if we now flag the feature #2 instead of the feature #1, we should see it loaded instead of the other:

to add the flag:

export const routes: Route[] = [
  {
    path: "feature-1",
    loadComponent: () =>
      import("./app/feature-1/feature-1.component").then(
        (m) => m.Feature1Component
      ),
-   data: { preload: true },
  },
  {
    path: "feature-2",
    loadChildren: () =>
      import("./app/feature-2/feature-2.route").then(
        (m) => m.routes
      ),
+   data: { preload: true },
  },
];
Enter fullscreen mode Exit fullscreen mode

FlagBasedPreloadingStrategy 2

Working as expected!

Takeaways

In this article, we saw what preloading strategies are and how they can help you to optimize the loading of your application to offer a smoother user experience by loading the required modules in the background, so the user doesn't have to wait for them to load when navigating to a new page.

By default, Angular provides two strategies:

However, in some business cases, you might want to orchestrate the loading of your lazy modules in a specific way. For such situations, implementing a custom preloading strategy might be the solution

If you would like to check the resulting code, you can head on to the associated

GitHub logo pBouillon / DEV.PreloadingStrategies

Demo code for the "Optimize your Angular app's user experience with preloading strategies" article


In a future article on this series, we will see how to articulate guards and preloading strategies, stay tuned!


I hope that you learn something useful there!


Photo by Joey Kyber on Unsplash

Top comments (3)

Collapse
 
younes_achachi profile image
younes achachi

ok

Collapse
 
digitaldino profile image
Dino Dujmovic

Nice one!

Collapse
 
rahulbenzeen profile image
RahulBenzeen

Very informative. Thanks