Telerik blogs

While code splitting speeds main page load time, lazy loading slows initial navigation to other routes. Ensure fast navigations by preloading route bundles.

Code splitting is a well-known technique we can use to divide our application’s JavaScript bundle into smaller bundles.

Instead of loading the entire application’s JavaScript in the initial bundle, we only include what is needed for the main page. We then lazy load the other route bundles.

While code splitting improves the application load speed and allows the users to start interacting with the main page quickly, lazy loading can slow down the initial navigation to other routes.

To ensure fast navigations as well, we need to take a further step after code splitting.

A calico cat looking up at some slender pink flowering plants, seeming like it might be getting ready to pounce.
[Photo credit]

Problems With Lazy Loading

The Angular Router lazy loads a route’s JavaScript bundle when the user visits that route. This means that when users visit a route for the first time, they have to wait for the browser to download, parse, compile and execute the JavaScript and then render the page.

The wait can be significantly longer on a slow network connection, leaving the user feeling frustrated. Chances are they will lose patience and abandon our site.

What can we do to make route navigations fast as well? ๐Ÿ˜€๐ŸŽŠ

Preload Route Bundles

The solution is quite simple really. ๐Ÿ˜‡ We don’t have to wait until the user visits a route to load its bundle. Instead, we can start preloading the route bundles behind the scenes, after the application’s main page has loaded.

Preloading a route means the browser will already download, parse and compile the route’s bundle. Leaving the browser to only execute the JavaScript and render the page when the user visits the route. This vastly improves the page’s load speed. ๐Ÿ•

How Does Preloading JavaScript Bundles Work in Angular?

The Router service has a preloader, RouterPreloader, which runs in the background.

When a navigation ends, the preloader goes through the routes in our application’s route configuration and loads the route’s bundle if the following conditions are met:

  • The route is configured for on-demand/lazy loading.
  • The route does not have a canLoad guard.
  • The route module has not been loaded yet.
  • The PreloadingStrategy registered with the Router service decides to load the route.

Route Configuration

Following is an example of a route configuration with a lazy-loaded route:

// Route Configuration

const routes: Routes = [{  
  path: 'feature-a',  
  loadChildren: () => import('./feature-a/feature-a.module').then(mod => mod.FeatureAModule),  
}];

The preloader executes the callback function specified in the loadChildren property to dynamically load the route’s module.

It is webpack that actually loads the module for us.

We often hear the terms preloading and prefetching when it comes to fetching resources (script, CSS, images) ahead of time.

The browser provides a prefetch mechanism, <link rel="prefetch">, to download resources that are likely to be needed in future navigations. The browser actually decides when the optimal time is to fetch the resource, taking into consideration the impact on the current page performance.

Additionally, there is a preload mechanism, <link rel="preload">, to download resources that are needed for the current navigation.

It is easy to start thinking the Angular preloader maybe uses either prefetch or preload behind the scenes. Perhaps prefetch as that is more suited for fetching resources that are likely to be needed in the next few navigations.

Also, when we read in webpack documentation about prefetching/preloading modules using the magic comments, we may start wondering if behind the scenes Angular adds the magic comments for us.

import(/* webpackPrefetch: true */ './feature-a/feature-a.module')
  .then(mod => mod.FeatureAModule),  
};

The result would be webpack adding a <link> tag in the head of our document:

<head>
  <link rel="prefetch" as="script" href="bundle-name.js">
</head>

However, this is not the case.

If we inspect our application, we can see (in the webpack/bootstrap ) that webpack adds a script tag to the document body, thus telling the browser to download the bundle. Following is part of the code:

var script = document.createElement('script');
script.src = jsonpScriptSrc(chunkId);
document.head.appendChild(script);

The Angular preloader simply checks to see if it should call import() for the dynamic/lazy routes that have not been loaded yet.

The route is dynamically imported after the current navigation has finished so it does not interfere with the current page load.

Why Does the Preloader Skip Routes With CanLoad Guard?

We can use canLoad guards to prevent dynamically loading routes that a user does not have access to.

A canLoad guard works fine to protect a lazy-loaded route, which is only loaded when the user visits the route.

However, a preloaded route is loaded before the user actually visits the route. As we saw earlier, the preloader loads the route bundles behind the scenes when a navigation has ended while the user is happily browsing the current route.

If the canLoad guard decided against preloading the route, it may navigate the user to a different route. This would be confusing and frustrating for the user as they didn’t initiate the navigation.

To prevent this, the Angular preloader does not preload any routes that has a canLoad guard on it.

We can use a canActivate guard instead to protect the routes that we want to preload.

ThecanActivate guard is run when the user actually visits the route and therefore it makes sense if the guard decides to navigate to another route when the user is not authorized to access that route.

Note: If a canLoad guard decides against loading the route, it either returns false to cancel the navigation or a UrlTree to cancel the current navigation and start a new navigation to that UrlTree.

The PreloadingStrategy Class

We mentioned earlier that the preloading strategy decides which modules to preload. The preloading strategy is represented by the PreloadingStrategy abstract class.

It has a single method called preload() which implements the logic for preloading a route.

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

When the current navigation ends, the preloader iterates over the routes in the Route Configuration and calls the preload() method of the registered preloading strategy.

It passes in two arguments:

  1. A route from the Route Configuration
  2. A callback function for loading the route

The preloading strategy is simply a service that implements the PreloadingStrategy class. We will look at how to create a preloading strategy in a bit.

Register the Preloading Strategy

To use the preloading strategy, we need to register it with the root Router service. The strategy applies to the whole application.

import { NgModule } from  '@angular/core';
import { Routes, RouterModule, PreloadAllModules } from '@angular/router';

const routes: Routes = []; // configure routes here

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

What Strategy Should We Use?

Once our application’s main page is loaded, we can start preloading the route bundles. The question is, what routes should we preload and when?

Let us first look at the preloading strategy Angular offers out of the box.

Preload All Modules

Angular has a PreloadAllModules strategy that optimistically preloads all modules as soon as the main application page has finished loading.

Preloading all the bundles certainly makes navigating to the lazily loaded routes faster. However, if we have a large application with quite a few modules, preloading them all has issues:

  • Our users will end up downloading bundles they don’t need.
  • It is costly for mobile users on limited data plans.
  • The bundles take a long time to download on slow network connections, affecting user experience.

In Angular, we can create a custom preloading strategy to provide the logic for which route modules to preload and when.

Custom Strategy

To create a custom preloading strategy, we create a service and implement the PreloadStrategy abstract class.

@Injectable({
  providedIn:  'root',
})
export  class  PreloadSelectedModules  implements  PreloadingStrategy {
  preload(route: Route, load: () =>  Observable<any>): Observable<any> {
    // logic for whether to preload this route module or not
  }
}

The trick is to figure out which modules to preload.

Demo ๐Ÿณ

It is time to get our feet wet and look at a demo. Let’s create a preloading strategy to address the problems highlighted earlier.

We will preload a route if:

  • The user is not on a slow network connection.
  • The user does not have the save data mode turned on.
  • We selected the route to be preloaded. Say we have some insight about which routes are popular.
export  class  PreloadSelectedModules  implements  PreloadingStrategy {
  preload(route: Route, load: () =>  Observable<any>): Observable<any> {
    if (slowConnection() || saveData()) return EMPTY;
    return  selectedForPreload(route) ? load() : EMPTY;
  }
}

Route data Property

As mentioned previously, the preloader iterates over the route configuration and calls preload() passing in the route. The Route object has a data property that lets us provide additional data for the route. We can use it to configure the routes we want to preload.

For example:

const routes: Routes = [
  { 
    path: 'feature-a', 
    loadChildren: () => import('./feature-a/feature-a.module')
						  .then(mod => mod.FeatureAModule),
    data: {
      preload: true
    }
  }
];  

In the preload() method we can read the route’s data property to see if the route is configured for preloading.

export  class  PreloadSelectedModules  implements  PreloadingStrategy {
  preload(route: Route, load: () =>  Observable<any>): Observable<any> {
    return  selectedForPreload(route) ? load() : EMPTY;
  }
  
  selectedForPreload(route): boolean {
    return route?.data?.preload;
  }
}

Network Information API โ„น๏ธ

The Network Information API provides us information about the user’s network connection and save data preference (for mobile users).

We can access it with:

const connection = navigator.connection;

If supported by the browser, navigator.connection will return an object which contains information about the effective network connection. We are especially interested in the effectiveType and saveData properties:

effectiveType ๐ŸŒ๐Ÿ‡

The effectiveType property tells us the effective type of the user’s network connection—that is, whether it is a ‘slow-2g’, ‘2g’, ‘3g’ or ‘4g’. This is determined by recent round-trip times and downlink values. For users connecting with WiFi, the connection is matched to the effective network connection type.

Round-trip time (RTT) is the time, measured in milliseconds, from when the browser sends a request to the server to when it receives a response from the server. downlink represents the effective bandwidth measured in megabits per second.

The code is pretty simple. ๐Ÿ˜€ We first check if navigator.connection is supported. Then we can check if the user is on a slow network connection. You can decide whether to include '3g' in the slow connections list or not.

slowConnection(): boolean {
  const connection = navigator.connection;
  const slowNetworks = ['slow-2g', '2g', '3g'];
  return connection ? slowNetworks.includes(this.connection.effectiveType) : false;
}

saveData ๐Ÿ’ฐ๐Ÿ’ธ

saveData returns true if the user has the save data option turned on.

saveData():boolean {
  const connection = navigator.connection;
  return connection?.saveData;
}

Browser Support โœ…โŒ

Although the Network Information API is incredibly useful, it is an experimental technology and currently not supported by Safari and Safari on iOS. It is available in Firefox behind the dom.netinfo.enabled preference (which needs to be set to true).

Nonetheless, we can check to see if the connection information is available and, if available, use it when preloading bundles to be mindful of the users’ connection and save data mode preference.

Speculative Preloading

In our demo, we manually selected the routes we wanted to prefetch. But we may not have insight into what routes to select. We could speculate on which route(s) the user is likely to visit next based on user behavior, like mouseover on a link, or based on the content of the page.

On mouseover/mousedown

When a user hovers over a route-link, we could preload that route in anticipation that the user is likely to click on it.

If we want to be more certain, we can use mousedown instead. There is still some time between mousedown and mouseup (making it a click).

InstantClick is an older JavaScript library that preloads resources on mouseover or mousedown.

We can implement our own version in Angular as shown by John Papa in his talk Preload Strategies.

Additionally, we can use the route configuration to blacklist routes that we don’t want to preload { data: { preload: none } } and exclude them in our preload() method.

Another speculative preloading strategy is to preload the routes that are visible on the current page, as these are the routes the user is likely to visit.

We can make use of the ngx-quicklink library that implements this for us!

ngx-quicklink:

  • uses the IntersectionObserver Web API to detect the links visible on the viewport
  • uses Navigation Information API to check the user’s network connectivity and save data mode
  • uses requestIdleCallback() API to preload the bundles when the browser is idle
  • creates the preloading strategy QuickLinkStrategy for us, so we can just register it with our router service

The setup is super simple. Please refer to Angular quicklink Preloading Strategy written by Minko Gechev.

Data-Driven Prefetching (Guess.js)

If we really want to take the guesswork out of deciding what route the user is likely to visit next, we can use Guess.js.

Guess.js:

  • uses data from analytics to learn about the user navigation patterns
  • uses machine learning to build a probability model for the pages a user is likely to visit from any given page
  • is mindful of the users’ network connection and save data mode preference
  • uses the browser’s prefetch mechanism, which utilizes browser idle time to prefetch the modules; the browser prefetches the modules and stores them in the cache, ready for when the user navigates to that page

Note, unlike ngx-quicklink, Guess.js does not create a service that implements the PreloadingStrategy, which we then register with Angular’s Router service.

On top of all the awesomeness Guess.js provides, it is also easy to set up and has a small runtime. This is not surprising since it is created by thought leaders in performance, tooling and innovation.

You can read more about the background and an in-depth explanation about the math and technologies behind Guess.js in this article by Minko Gechev.

Conclusion

Code splitting and lazy loading improve our application load speed; however, the initial load speed is not all that matters. Lazy loading feature modules slows navigations, especially on slow network connections, as the user has to wait for the bundle to be downloaded and processed. Not to worry, though. We can preload the route modules for fast navigations.

The Angular Router provides a PreloadAllModules strategy. However, preloading too many bundles can slow the application down, cost the users in data and hinder the application’s interactivity. We need to be mindful of our users’ network connection and save data mode preference.

Angular also lets us create a custom preloading strategy. The challenge is to preload the routes the user is most likely to visit next. We can use speculative preloading and analytics data. We can also make use of libraries like ngx-quicklink and Guess.js.

You can see the full code here and play with the code. Here are some suggestions:

  1. Change the preloading strategy in the Router ExtraOptions to try out the strategies we talk about in this post:
  • PreloadAllModules
  • PreloadSelectedModules
  • QuicklinkStrategy
  • PreloadHoveredModules
  1. See the JavaScript bundles being loaded in Network tab in the Chrome DevTools.

  2. Throttle the network to a slower network to see that QuicklinkStrategy and PreloadSelectedModules do not preload modules on a slow network connection.

Network shows a caution icon, revealing Slow 3G.


ashnita-bali
About the Author

Ashnita Bali

Ashnita is a frontend web developer who loves JavaScript and Angular. She is an organizer at GDGReading, a WomenTechmakers Ambassador and a mentor at freeCodeCampReading. Ashnita is passionate about learning and thinks that writing and sharing ideas are great ways of learning. Besides coding, she loves the outdoors and nature.

Related Posts

Comments

Comments are disabled in preview mode.