Table of Contents
- Why performance optimization is important
- Why do preloading strategies matter?
- Default preloading strategies in Angular
- Using preloading strategies
- Implementing our own preloading strategy
- Takeaways
Why performance optimization is important
For any website or web application, performance is a cornerstone of good user experience — often abbreviated as “UX.”
Think back to the last time you sat waiting for a page to appear. Whatever the site was, I’d bet the memory isn’t a fond one.
Generally speaking, users have little patience for sluggish websites. Slow loading can breed annoyance or even anxiety — questions like “Did my progress get saved?” or “Is it safe to enter my details here?” quickly surface.
The more responsive and quick a site is, the more reliable and enjoyable it feels. Its speed can even shape how people view your brand; a laggy, unresponsive app may leave a sour impression and discourage repeat visits.
Why do preloading strategies matter?
When performance comes up in Angular, lazy loading is usually the first thing mentioned.
By breaking your application modules features into distinct chunks, you can fetch them only when needed, rather than packaging everything together and requiring users to download the full app up front.
However, business logic sometimes tells you that certain lazy modules are almost guaranteed to be visited soon. Preloading those modules makes sense: it prevents a noticeable delay when the user eventually clicks through.
Angular handles these cases with what it calls preloading strategies.
A preloading strategy instructs the Angular router to fetch modules in the background before they’re actually needed. That way, when navigation happens, the necessary modules are already available and the page renders nearly instantly.
A preloading strategy is just an Angular class extending the PreloadingStrategy abstract class, which looks like this:
abstract class PreloadingStrategy {
abstract preload(route: Route, fn: () => Observable<any>): Observable<any>
}
When called, this method returns fn when the route should be loaded ahead of time, or an Observable<null> otherwise.
Default preloading strategies in Angular
NoPreloading
By default, Angular’s lazy-loading setup never preloads anything.
This relies on the NoPreloading Strategy, which the official docs describe as:
Provides a preloading strategy that does not preload any modules.
In practice, this means a module is fetched only when its route is visited:
The downside is that subsequent page loads can feel slower as navigation continues.
PreloadAllModules
The opposite approach is the PreloadAllModules Strategy which, as the name implies, loads every module up front:
Provides a preloading strategy that preloads all modules as quickly as possible.
With this strategy, everything is pulled in at once:
Whichever of these built-ins you pick, neither offers much nuance when you want to control precisely when and how your app gets loaded.
That’s where custom preloading strategies come in.
Using Preloading Strategies
To tell the router which preloading strategy it should use, you assign it to the preloadingStrategy option within the RouterModule.forRoot() call in your root module:
@NgModule({
imports: [RouterModule.forRoot(routes, {
preloadingStrategy: /* ... */
})],
})
export class AppModule { }
If you’re using Standalone Components, you’ll instead want a provider wired in during app bootstrap.
That setup is done by adding withPreloading to the provideRouter configuration:
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.
Building a custom preloading strategy
Earlier, we touched on why you might want to write your own preloading logic. Let's walk through that process now.
What we're aiming for
Our strategy will be straightforward: it will only preload lazy-loaded routes whose data object contains a preload property set to true.
Adjusting the routes
First, I need to update the route definitions from the earlier examples by adding this new property:
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 this flag in place, our custom logic should fetch "Feature #1" immediately, while leaving "Feature #2" for later.
The implementation
The core of a preloading strategy is a service that implements the PreloadingStrategy interface.
Let's build 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);
}
}
We register it in
rootso the router can access it during application bootstrap.
The actual logic is the easy part. We look at the current route's configuration; if our flag is found, we trigger the load function. If not, the module stays untouched.
Now we just need to tell the router to use our strategy. This is done at bootstrap, just like with the built-in ones:
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes, withPreloading(FlagBasedPreloadingStrategy))
],
});
Check the network tab in your browser's devtools now. You'll see that feature #1 has been fetched, but feature #2 hasn't:
To prove it's the flag that matters, let's switch which feature is marked for preloading. After updating 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 },
},
];
Now feature #2 should be loaded instead.

It works.
Key points
Preloading strategies are a tool for improving perceived performance. By fetching lazy modules in the background, you can reduce the wait time when a user navigates to a new part of your app.
Angular ships with two default strategies:
NoPreloading, which disables preloading entirely.PreloadAllModules, which loads every lazy module upfront.
There are times when neither of these fits your needs. Maybe you have specific modules that are critical for a primary user flow, or you want to load modules based on user behavior. In these cases, a custom strategy gives you complete control over when and how your modules are loaded.
You can find the full code example in the accompanying
pBouillon / DEV.PreloadingStrategies
Demo code for the "Optimize your Angular app's user experience with preloading strategies" article
Demo code for the "Optimize your Angular app's user experience with preloading strategies" article on DEV
The next article in this series will discuss how guards and preloading strategies can work together.
I hope this has been helpful!
Photo by Joey Kyber on Unsplash



