Custom Preloading Strategy in Angular

Preloading modules in Angular allows applications to load lazy-loaded modules before they are needed, improving perceived performance and user experience.
By default, Angular provides two strategies for preloading modules:

  1. No Preloading – Lazy modules are loaded only when the user navigates to them.
  2. PreloadAllModules – All lazy-loaded modules are fetched in the background immediately after the application loads.

However, in complex applications, developers often need fine-grained control over which modules to preload.
Angular allows this using a custom preloading strategy.

This guide provides a detailed explanation of custom preloading strategies, including how to implement and apply them effectively.

1. Introduction to Module Preloading

Lazy-loaded modules are a powerful tool in Angular for splitting applications into smaller, load-on-demand chunks.
However, lazy loading can cause delays when a user navigates to a route for the first time. Preloading solves this by fetching modules in the background after the main app loads, improving navigation performance.

Preloading Strategies in Angular:

  • NoPreloading: Default behavior; lazy-loaded modules are only loaded when requested.
  • PreloadAllModules: All lazy-loaded modules are loaded after the app initialization.
  • Custom Preloading Strategy: Allows selective module preloading based on criteria defined by the developer.

2. Why Use a Custom Preloading Strategy?

  • Some modules are critical for user experience and should be preloaded.
  • Other modules are rarely used, so preloading them wastes bandwidth.
  • Custom strategies allow conditional, dynamic, or prioritized preloading.
  • Helps balance performance and bandwidth in large applications.

3. Implementing a Custom Preloading Strategy

Angular provides the PreloadingStrategy interface.
A custom strategy implements its preload method:

import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of } from 'rxjs';
import { Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class SelectivePreloadingStrategy implements PreloadingStrategy {
  preload(route: Route, load: () => Observable<any>): Observable<any> {
return route.data &amp;&amp; route.data&#91;'preload'] ? load() : of(null);
} }

Explanation:

  • route: Contains metadata about the route being processed.
  • load: Function to fetch the lazy module.
  • Checks if route.data['preload'] is true; if yes, loads the module, otherwise returns of(null) to skip preloading.

4. Applying the Strategy in Routes

To use the custom preloading strategy, add data: { preload: true } to the lazy-loaded route:

const routes: Routes = [
  {
path: 'admin',
loadChildren: () =&gt; import('./admin/admin.module').then(m =&gt; m.AdminModule),
data: { preload: true }
}, {
path: 'reports',
loadChildren: () =&gt; import('./reports/reports.module').then(m =&gt; m.ReportsModule),
data: { preload: false }
} ];

Here:

  • admin module will be preloaded.
  • reports module will not be preloaded.

5. Registering the Custom Preloading Strategy

You need to tell Angular to use your strategy in the routing module:

@NgModule({
  imports: [
RouterModule.forRoot(routes, { preloadingStrategy: SelectivePreloadingStrategy })
], exports: [RouterModule] }) export class AppRoutingModule {}

With this setup, Angular will selectively preload modules based on the preload flag in route data.


6. Using Multiple Criteria for Preloading

You can make the strategy more advanced by checking multiple properties or even user roles:

@Injectable({ providedIn: 'root' })
export class AdvancedPreloadingStrategy implements PreloadingStrategy {
  preload(route: Route, load: () => Observable<any>): Observable<any> {
const preloadFlag = route.data &amp;&amp; route.data&#91;'preload'];
const userRole = route.data &amp;&amp; route.data&#91;'role'];
if (preloadFlag &amp;&amp; userRole === 'admin') {
  return load();
}
return of(null);
} }

This approach allows:

  • Conditional preloading based on user roles.
  • Preloading only modules relevant to the current user.

7. Preloading Multiple Modules Dynamically

Sometimes you may want to preload a list of modules dynamically:

@Injectable({ providedIn: 'root' })
export class DynamicPreloadingStrategy implements PreloadingStrategy {
  private modulesToPreload: string[] = ['admin', 'dashboard'];

  preload(route: Route, load: () => Observable<any>): Observable<any> {
if (route.path &amp;&amp; this.modulesToPreload.includes(route.path)) {
  return load();
}
return of(null);
} }
  • Only modules listed in modulesToPreload are preloaded.
  • The rest remain lazy-loaded on demand.

8. Using Observables and Delays

You can delay preloading to avoid network congestion:

import { delay } from 'rxjs/operators';

@Injectable({ providedIn: 'root' })
export class DelayedPreloadingStrategy implements PreloadingStrategy {
  preload(route: Route, load: () => Observable<any>): Observable<any> {
if (route.data &amp;&amp; route.data&#91;'preload']) {
  return load().pipe(delay(5000)); // preload after 5 seconds
}
return of(null);
} }
  • Useful for large apps where preloading all modules immediately could impact performance.
  • Delays help stagger background loading.

9. Logging Preloaded Modules

You can log which modules are preloaded for debugging:

@Injectable({ providedIn: 'root' })
export class LoggingPreloadingStrategy implements PreloadingStrategy {
  preload(route: Route, load: () => Observable<any>): Observable<any> {
if (route.data &amp;&amp; route.data&#91;'preload']) {
  console.log(Preloading module: ${route.path});
  return load();
} else {
  console.log(Skipping module: ${route.path});
  return of(null);
}
} }

This provides visibility into the preloading process during development.


10. Handling Errors in Preloading

You can catch errors to prevent the app from crashing if a module fails to load:

import { catchError } from 'rxjs/operators';

@Injectable({ providedIn: 'root' })
export class SafePreloadingStrategy implements PreloadingStrategy {
  preload(route: Route, load: () => Observable<any>): Observable<any> {
if (route.data &amp;&amp; route.data&#91;'preload']) {
  return load().pipe(
    catchError(err =&gt; {
      console.error(Error preloading ${route.path}:, err);
      return of(null);
    })
  );
}
return of(null);
} }
  • Ensures preloading failures don’t block the application.
  • Helps maintain a smooth user experience.

11. Combining Custom Preloading with Auth Guards

Custom preloading can be combined with guards to preload modules only if the user has access:

@Injectable({ providedIn: 'root' })
export class AuthPreloadingStrategy implements PreloadingStrategy {
  constructor(private auth: AuthService) {}

  preload(route: Route, load: () => Observable<any>): Observable<any> {
if (route.data &amp;&amp; route.data&#91;'preload'] &amp;&amp; this.auth.isLoggedIn) {
  return load();
}
return of(null);
} }
  • Admin or authenticated users get preloaded modules.
  • Unauthorized users skip preloading to save bandwidth.

12. Preloading with Route Resolvers

You can combine preloading with Resolve guards:

const routes: Routes = [
  {
path: 'dashboard',
loadChildren: () =&gt; import('./dashboard/dashboard.module').then(m =&gt; m.DashboardModule),
data: { preload: true },
resolve: { config: ConfigResolver }
} ];
  • Module is preloaded.
  • Data required by the module is resolved beforehand.

13. Lazy Loading vs Preloading Strategy

FeatureLazy LoadingPreloading
When loadedOn-demandAfter app initialization
Impact on performanceInitial load is smallerFaster route navigation later
Best forRarely used featuresFrequently accessed modules
Controlled byloadChildrenPreloadingStrategy

14. Advanced: Priority-Based Preloading

You can assign priority levels to modules:

@Injectable({ providedIn: 'root' })
export class PriorityPreloadingStrategy implements PreloadingStrategy {
  preload(route: Route, load: () => Observable<any>): Observable<any> {
const priority = route.data &amp;&amp; route.data&#91;'priority'] || 0;
if (priority &gt; 0) {
  return load();
}
return of(null);
} }

Routes with higher priority values can be preloaded first using custom logic or delays.

data: { preload: true, priority: 10 }

15. Full Example of Custom Preloading

Step 1: Define Strategy

@Injectable({ providedIn: 'root' })
export class SelectivePreloadingStrategy implements PreloadingStrategy {
  preload(route: Route, load: () => Observable<any>): Observable<any> {
return route.data &amp;&amp; route.data&#91;'preload'] ? load() : of(null);
} }

Step 2: Define Routes

const routes: Routes = [
  { path: '', component: HomeComponent },
  {
path: 'admin',
loadChildren: () =&gt; import('./admin/admin.module').then(m =&gt; m.AdminModule),
data: { preload: true }
}, {
path: 'reports',
loadChildren: () =&gt; import('./reports/reports.module').then(m =&gt; m.ReportsModule),
data: { preload: false }
} ];

Step 3: Register Strategy

@NgModule({
  imports: [RouterModule.forRoot(routes, { preloadingStrategy: SelectivePreloadingStrategy })],
  exports: [RouterModule]
})
export class AppRoutingModule {}

This simple setup allows selective module preloading based on the preload flag.


16. Benefits of Custom Preloading

  1. Faster route navigation: Frequently used modules are already loaded.
  2. Bandwidth optimization: Rarely used modules are loaded on demand.
  3. User experience improvement: Reduces waiting time for critical modules.
  4. Fine-grained control: Preload modules based on roles, priorities, or other conditions.
  5. Easier debugging and logging: You can track which modules are preloaded.

17. Testing Custom Preloading

You can test whether modules are preloaded using:

it('should preload only flagged modules', () => {
  const strategy = new SelectivePreloadingStrategy();
  const loadFn = jasmine.createSpy('loadFn').and.returnValue(of(true));

  const routePreload = { path: 'admin', data: { preload: true } } as Route;
  const routeSkip = { path: 'reports', data: { preload: false } } as Route;

  strategy.preload(routePreload, loadFn).subscribe();
  expect(loadFn).toHaveBeenCalled();

  strategy.preload(routeSkip, loadFn).subscribe();
  expect(loadFn).toHaveBeenCalledTimes(1); // Only called for admin
});
  • Ensures preloading logic behaves as expected.
  • Verifies conditional loading.

18. Best Practices for Custom Preloading

  1. Preload only frequently accessed modules to save bandwidth.
  2. Use route data flags for simplicity.
  3. Combine with guards to respect authorization.
  4. Avoid long-running preloading tasks that block main navigation.
  5. Log preloaded modules during development for monitoring.
  6. Use delay or priority if multiple modules compete for network resources.
  7. Test strategy thoroughly to ensure proper behavior.

19. When Not to Use Preloading

  • Modules are rarely accessed and very large.
  • Users are on slow network connections.
  • Critical initial bundle size must be minimal.

In such cases, rely solely on lazy loading.


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *