Optimize your Angular app's user experience with preloading strategies
Tags: angular, webdev, tutorial, javascript
Published: 2023-05-02 04:21:11

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](#why-performance-optimization-is-important)
1. [Why do preloading strategies matter?](#why-do-preloading-strategies-matter)
1. [Default preloading strategies in Angular](#default-preloading-strategies-in-angular)
1. [Using preloading strategies](#using-preloading-strategies)
1. [Implementing our own preloading strategy](#implementing-our-own-preloading-strategy)
1. [Takeaways](#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](https://angular.io/guide/lazy-loading-ngmodules).

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](https://angular.io/api/router/PreloadingStrategy).

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](https://angular.io/api/router/PreloadingStrategy) defined as such:

```ts
abstract class PreloadingStrategy {
  abstract preload(route: Route, fn: () => Observable<any>): Observable<any>
} 
```

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](https://angular.io/api/router/NoPreloading) 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sed8gwgk8atct8iqxas5.gif)

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](https://angular.io/api/router/PreloadAllModules) 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q5n70pxhmyah9wigxizu.gif)

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:

```ts
@NgModule({
  imports: [RouterModule.forRoot(routes, {
    preloadingStrategy: /* ... */
  })],
})
export class AppModule { }
```

However, if you are now using [Standalone Components](https://angular.io/guide/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`:

```ts
bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes, withPreloading(/* ... */))
  ],
});
```

> 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:

```ts
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
      ),
  },
];
```

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:

```ts
// 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);
  }
}
```

> 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:

```ts
bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes, withPreloading(FlagBasedPreloadingStrategy))
  ],
});
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t6oxflir1ngllnc8mp14.gif)

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:

```diff
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 },
  },
];
```

![FlagBasedPreloadingStrategy 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qi5peyv82rad1kxurewj.gif)

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:
- A [`NoPreloading` Strategy](https://angular.io/api/router/NoPreloading) that does not load any lazy route
- A [`PreloadAllModules` Strategy](https://angular.io/api/router/PreloadAllModules) that does the opposite

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 {% embed https://github.com/pBouillon/DEV.PreloadingStrategies %}

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!

{% embed https://dev.to/pbouillon %}

---

> Photo by <a href="https://unsplash.com/@jtkyber1?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Joey Kyber</a> on <a href="https://unsplash.com/photos/45FJgZMXCK8?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>