Using preloading in Angular apps to reduce ChunkLoadingError

Reducing the amount of ChunkLoadingError

Ali Kamalizade
4 min readJun 26, 2023
Photo by Mike van den Bos on Unsplash

Our Angular-based frontend is divided into different self-contained modules (e.g. inbox, document center, dashboard…). Those modules are typically rather small and loaded asynchronously. The benefits of splitting up the application into modules:

  • Avoid loading stuff of pages you’re not visiting
  • Initial loading times are improved as we only load the necessary code initially
  • The modules are being kept as small as possible which makes it easier to work on a particular area without affecting other product areas

When a new deployment is published in production all these modules get a hash. This prevents browsers to cache old versions of code which changes frequently (as the hash is different every time the browser will download the new modules instead of the cached modules from the past). New modules in, old modules out. This is great for static pages but …

How does this affect single page applications? Well, as long as we do not reload the page the references to the old modules are still there. If we now attempt to navigate to one of those unloaded modules, the browser attempts to load a JavaScript bundle (aka file) which may not exist anymore depending on your web hosting platform (e.g. Netlify). In case the bundle does not exist anymore Angular will throw a ChunkLoadError exception. The only thing a user can do is to reload the page but this sours the user experience.

ChunkLoadError exception

Solution: preloading. The idea: if the user is logged in we load the modules silently in the background after a delay. Benefits:

  • Initial loading are still as good as before as we only load the necessary code initially for bootstrapping the application
  • Switching to other pages for the first time is a bit faster as the modules might have already been loaded
  • Greatly reduced chance of having the ChunkLoadError exception again

Let’s explore how we can use preloading in our Angular application to make it less likely to run into ChunkLoadError exceptions.

How to preload modules using custom preloading strategy class

Let’s create a service to handle preloading. It implements the PreloadingStrategy interface.

export class CustomPreloadingStrategyService implements PreloadingStrategy {
constructor(private authService: AuthService) {}

preload(route: Route, loadFn: () => Observable<any>) {
const initialDelayInMs = 60_000;
return timer(initialDelayInMs).pipe(
first(),
// swap with your own implementation
switchMap(() => this.authService.isLoggedIn()),
switchMap((isLoggedIn) => {
// skip preloading if there is no internet connection
if (!window.navigator.onLine) {
return of(null);
} else if (isLoggedIn && !route.data?.skipPreload) {
// user is logged in and route is preloadable
return loadFn().pipe(
catchError((err) => {
console.warn('Module could not be preloaded', err);
return of(null);
})
);
} else {
// no preloading: continue
return of(null);
}
})
);
}
}

Some notes regarding the code:

  • It can be a good idea to delay the preloading.
  • Internet connection can be flaky, especially on mobile. Preloading should not throw an error when the user is temporarily offline. Therefore, we can check if the user is online and cancelling the preloading if the user is offline.
  • Some routes may not need to be preloaded. E.g. it might not be necessary to preload the login module if the user is already logged in as the user would never go to the login page until they log out.

After we have created our custom preloading strategy class, we need to refer to it when setting up routing.

// routes
{
path: 'dashboard',
loadChildren: () => import('./dashboard/dashboard.module').then((m) => m.DashboardModule),
data: {}
},
{
path: 'login',
loadChildren: () => import('./login/login.module').then((m) => m.LoginModule),
data: { skipPreload: true }
}

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

Bonus point: in case a ChunkLoadError does occur we can soften the impact. By providing a custom error handler to our application, we can execute code whenever an exception is thrown.

  • If the exception is a ChunkLoadError we show a user-friendly reminder to reload the page
  • Else we show a general red toastr message to let the user know that an uncaught error has occurred
interface ErrorWithReason extends Error {
rejection?: { message: string };
}

@Injectable({ providedIn: 'root' })
export class CustomErrorHandlerService implements ErrorHandler {
constructor(private toastrService: ToastrService, private zone: NgZone) {}

handleError(err: Error) {
console.error(err);
this.zone.run(() => {
if (isChunkLoadingError(err)) {
this.toastrService.control('Please reload the page', 'A new version is available');
} else {
this.toastrService.danger('An unhandled error occurred', 'Please reload the page and try again');
}
});
}
}

// if the error is a ChunkLoadError we want users to reload the page to get the latest version
function isChunkLoadingError(err: ErrorWithReason) {
return err.message.includes('ChunkLoadError: Loading chunk');
}

Conclusion

Thanks for reading this post about preloading in Angular applications to reduce the likelihood for ChunkLoadError exceptions to occur. Angular provides simple hooks to create simple or sophisticated preloading strategies. And as a side bonus we also got reduced time it takes to navigate between different parts of the applications. What measures are you taking? Let me know in the comments.

--

--

Ali Kamalizade
Ali Kamalizade

Written by Ali Kamalizade

Co-founder of Sunhat. Posts about software engineering, startups and anything else. 有難うございます。🚀

No responses yet