Predictive Preloading Strategy for Your Angular Bundles
Users want fast apps. Getting your JavaScript bundles to your browser as quickly as possible and before your user needs them can make a huge and positive impact on their user experience. Knowing how you can improve that experience is important.
One way you can improve user experience with your Angular apps is to strategically decide which bundles to preload. You control when your bundles load and which bundles load. This is why you should explore choosing a built-in or creating your own custom Angular preload strategy.
In this series we'll explore a few of your options for preloading Angular bundles.
Here are the articles in this series
- Preload all Angular Bundles
- You Pick Which Angular Bundles to Preload
- Preload Angular Bundles When Good Network Connectivity is Detected
- Predictive Preloading Strategy for Your Angular Bundles
Scouting Ahead
The "on demand" strategy preloads one or more routes when a user performs a specific action. You decide which action will cause a route to preload. For example, you could set this up to preload a route while a user hovers over a button or menu item.
You can create the custom OnDemandPreloadService
by creating a class that implements the PreloadingStrategy
interface, and providing it in the root. Then you must implement the preload
function and return the load()
function when you want to tell Angular to preload the function.
Notice the preload
function in the class OnDemandPreloadService
examines the Observable preloadOnDemand$
. It pipes the observable and uses the mergeMap
RxJs operator to switch to a new Observable. This new Observable's value depends on the local preloadCheck
function.
The preloadCheck
function checks if the preloadOptions
(which comes from the original Observable) has a routePath
that matches a route that has the data.preload
property set to true
. So here we are opting some of the routes into preloading and leaving some routes to be loaded when they are requested explicitly.
@Injectable({ providedIn: 'root', deps: [OnDemandPreloadService] })
export class OnDemandPreloadStrategy implements PreloadingStrategy {
private preloadOnDemand$: Observable<OnDemandPreloadOptions>;
constructor(private preloadOnDemandService: OnDemandPreloadService) {
this.preloadOnDemand$ = this.preloadOnDemandService.state;
}
preload(route: Route, load: () => Observable<any>): Observable<any> {
return this.preloadOnDemand$.pipe(
mergeMap(preloadOptions => {
const shouldPreload = this.preloadCheck(route, preloadOptions);
return shouldPreload ? load() : EMPTY;
})
);
}
private preloadCheck(route: Route, preloadOptions: OnDemandPreloadOptions) {
return (
route.data &&
route.data['preload'] &&
[route.path, '*'].includes(preloadOptions.routePath) &&
preloadOptions.preload
);
}
}
Route Definitions
This strategy requires that you indicate which routes can be preloaded. You can do this by adding the data.preload
property and set it to true
in your route definition, as shown below.
export const routes: Routes = [
{ path: '', pathMatch: 'full', redirectTo: 'heroes' },
{
path: 'dashboard',
loadChildren: () =>
import('app/dashboard/dashboard.module').then(m => m.DashboardModule),
data: { preload: true }
},
{
path: 'heroes',
loadChildren: () =>
import('app/heroes/heroes.module').then(m => m.HeroesModule),
data: { preload: true }
},
{
path: 'villains',
loadChildren: () =>
import('app/villains/villains.module').then(m => m.VillainsModule)
},
{ path: '**', pathMatch: 'full', component: PageNotFoundComponent }
];
Notice that the dashboard and heroes routes both have the preload.data
property set to true
. However, the villains route does not have this property set. In this scenario the heroes and dashboard have preloading enabled, but the villains would only load when the user navigates to this route.
Setting the Custom OnDemandPreloadService
Then when setting up your RouterModule
, pass the router options including the preloadingStrategy
to the forRoot()
function.
@NgModule({
imports: [
RouterModule.forRoot(routes, {
preloadingStrategy: OnDemandPreloadService
})
],
exports: [RouterModule]
})
export class AppRoutingModule {}
Deciding When to Preload
The missing piece here is the mechanism that you use to tell the app which route to preload and when to preload it. Notice the service OnDemandPreloadService
in the code below. You can call this service's startPreload
function and pass the route you wish to preload. The OnDemandPreloadService
service then next's the subject (think of this like publishing or emitting a message). Then whoever or whatever listens to that message can act on it.
This is where the OnDemandPreloadStrategy
strategy comes in, as it is listening.
export class OnDemandPreloadOptions {
constructor(public routePath: string, public preload = true) {}
}
@Injectable({ providedIn: 'root' })
export class OnDemandPreloadService {
private subject = new Subject<OnDemandPreloadOptions>();
state = this.subject.asObservable();
startPreload(routePath: string) {
const message = new OnDemandPreloadOptions(routePath, true);
this.subject.next(message);
}
}
Bind to a Mouseover Event
Now your app is ready to preload a route when you decide to do it. You can try this by binding a DOM event such as mouseover
and firing the OnDemandPreloadService
's startPreload
function.
<a
[routerLink]="item.link"
class="nav-link"
(mouseover)="preloadBundle('heroes')"
>heroes</a
>
Notice the following code accepts the route path and passes it along to the preloadOnDemandService.startPreload
function.
preloadBundle(routePath) {
this.preloadOnDemandService.startPreload(routePath);
}
All Together
Let's step back and follow how this all works.
- A user hovers over your anchor tag
- The
mouseover
binding calls a function in your component, passing the route path ('heroes' in this case) - That code calls the
PreloadOnDemandService
service'sstartPreload
, passing the route path to it - The
PreloadOnDemandService
service next's the RxJS Subject, which is exposed as an Observable - The
OnDemandPreloadStrategy
gets a handle on that Observable, and it knows when it "nexts" - The
OnDemandPreloadStrategy
pipes it intomergeMap
and evaluates the route for preloading - If it decides to preload, the
OnDemandPreloadStrategy
returns a new Observable with theload()
function - If it decides not to preload, the
OnDemandPreloadStrategy
returns an Observable with theEMPTY
observable (which does not preload) - The Angular router listens to the response of the strategy's
preload
function and either preloads or not, accordingly.
Try It
After applying this strategy, rebuild and run your app with ng serve
. Open your browser, open your developer tools, and go to http://localhost:4200
. When you inspect the Network tab in your browser you will likely see none of your bundles already preloaded (except whichever route your navigated to by default, if that was lazy loaded).
Then hover over the HTML element where it fires with the mouseover
event you bound. Check your network tab in your browser and you will see the bundle will be preloaded.
Deciding What Is Right For Your App
Now that you know how to create your own preload strategy such as OnDemandPreloadService
, how do you evaluate if this is the right strategy for your app?
This is a more involved strategy for certain. Could it be beneficial to your users? Do your users often hover over search results before selecting them? Would that normally fire off a lazy loaded bundle? If so, perhaps this could give that preload a jumpstart.
If you can determine that your users' behavior and workflow often follows a specific path before loading a new bundle, then this strategy could be beneficial.
You can apply this to a number of scenarios such as hovering over an HTML element, clicking a button, or scrolling to a specific area of the screen.
In the end the decision is up to you. I recommend before choosing this options, or any preload strategy, that you test at various network speeds under various valid and common user workflows. This data will help you decide if this is the right strategy for you, or if another may be more beneficial for users of your app.
Resources
- Get VS Code
- Get the VS Code Angular Essentials
- Get the VS Code Angular Snippets