Introduction to the Angular Router, Lazy Loading and Prefetching

What does Angular Router do?

The router takes a URL and the router configuration that the developer provides. The purpose of using the configuration is to construct the router out of the URL then it uses the state object to instantiate the components. When the user clicks on a link, the router updates the URL and constructs a new router and repeat the whole process once again. It compares the old state to the new state and updates the components accordingly.

0.jpg

The primary job of a router is to manage the navigation between states which includes updating the URL and the component tree.

Example

To illustrate, a mail application is developed similar to Gmail. There are two sections in the application; messages and contacts. The conversations section is displayed and if you click on the context button, the second part of the application appears which is a list of contacts.

0-0.jpg

Each of case is represented by two components and, from the router's perspective, the router does not know about components. The following is the router configuration.

const ROUTES = [

  {

    path: 'conversations/:folder',

    children: [

      { path: '', component: ConversationsCmp },

      { path: ':id', component: ConversationCmp, children: […]}

    ]

  },

 {

    path: 'contacts',

    children: [

      { path: '', component: ContactsCmp },

      { path: ':id', component: ContactCmp }

    ]

  }

];

The first route describes the conversations part of the application. The router will take the configuration and will match it against the URL. This particular router, for instance, will match these URLs. The functionality of a router has a very powerful URL matching engine.

In the next step, we take this configuration and pass it to router module for the root which is responsible for creating the router, and for aiding the directive to the current compilation context.

@Component({...}) class MailAppCmp {}

@Component({...}) class ConversationsCmp {}
@Component({...}) class ConversationCmp {}

@Component({...}) class ContactsCmp {}
@Component({...}) class ContactCmp {}
const ROUTES = [
{
  path: 'conversations/:folder', children: [
   { path: '', component: ConversationsCmp },
   { path: ':id', component: ConversationCmp }
]
},
conversations/inbox - ConversationsCmp
conversations/inbox/33 - ConversationCmp

{
  path: 'contacts', children: [
{ path: '', component: ContactsCmp },
{ path: ':id', component: ContactCmp }
]
}
];
contacts - ContactsCmp
contacts/44 - ContactCmp

@NgModule({
    bootstrap: [MailAppCmp], 
    imports: [RouterModule.forRoot(ROUTES)]
})
class MailModule {}

Links and Navigation

We also have the navigation functionality and it can also be done by using a router. With the router configuration, we can parse the URL into the router state. After this, we use router links to navigate new URLs and repeat this process.

1.jpg

Lazy Loading

6

Lazy loading or loading on demand enhances the speed of the load time in an application by breaking into multiple packs and loading them when demanded. It is a key capability of the new router. The router is specially developed to make lazy loading simple at an ease.  For instance, the router is made as URL-based so it could generate own links.

4.jpg
5.jpg

One Problem

The above process works fine with the configuration, but it has one problem. The router configuration refers to all the component classes, and we are required to have a single class even though the component and the counter component are displayed on load. They are still bundled up as the main part of the application. As a consequence, the initial bundle is larger than it was estimated. Due to this, it takes longer to download, to parse and to bootstrap.

main.bundle.js

MailAppCmp

ConversationsCmp

ConversationCmp

ContactsCmp

ContactCmp

Two extra components are not like a big deal, but when using a real application the contact module may have hundreds of components. It is operating all together with the services and helper.

Solution – LoadChildren

The best solution is to extract the contacts-related code into a separate module in a setup and load it on-demand. The first step is to extract all the context-related components and routes into a separate module. For example, following is the configuration from the main file.

@Component({…}) class ContactsComponent {}

@Component({…}) class ContactComponent {}

 

const ROUTES = [

  { path: '', component: ContactsComponent },

  { path: ':id', component: ContactComponent }

];

 

@NgModule({

  imports: [RouterModule.forChild(ROUTES)]

})

class ContactsModule {}
2.jpg

Since we are now creating child modules, we can isolate the code for those modules into their own packages / binaries. When a particular route is accessed, the Angular routing library loads in the code for that module and then executes and renders the view. The next step is to update the main module to the newly extracted one.  The chosen property should be replaced with `loadChildren`. `loadChildren` tells the router to fetch contacts.bundle.js which will later combine with its configuration with the main file and then activates the compliance. The token is accepted and is passed to a module-loader. The functionality of the module-loader is to load the configuration. File system.js is the default module-loader, but we can use a different method for loading code.

const ROUTES = [

  {

    path: 'conversations/:folder',

    children: [

      {

        path: '',

        component: ConversationsCmp

      },

      {

        path: ':id',

        component: ConversationCmp,

        children: […]

      }

    ]

  },

  {

    path: 'contacts',

    loadChildren: 'contacts.bundle.js',

  }

];

At launch, only the components are displayed and all the context-related code has been moved from the main bundle to a separate bundle. When using `loadChildren` property, we first get the main bundle and the router won't load the second bundle. As a result, the main bundle is smaller, so it downloads quickly and takes less time to bootstrap. All links, navigation and the components are working the similar way as described.

3.jpg

The system is lazy-loading transparent because we can opt in or out without making any change to the component with a minimal effort.

Preloading

8.jpg

The context module won't be looped unless the user clicks on the link which has a small bundle and it downloads quickly. The problem is when the user clicks the button and the router has to fetch the module from the server which may take some time. The solution is to download the bundle after the critical assets for the current view have been downloaded. This whole concept is called pre-loading.

7.jpg

@NgModule ({

bootstrap: [MailAppCmp],

imports: [RouterModule.forRoot(ROUTES,

{preloadingStrategy: PreloadAllModules})]

])

class MailModule {}

 

Everything has been downloaded in the background and finally when the link has been clicked, the navigation instantly takes place. We can enable pre-loading by passing its strategy to the router and the latest version of the router ships with two strategies: preloading nothing and preloading everything. The two building strategies are one-liners.

In custom preloading strategy, we explicitly tell the router configuration what should be preloaded by the following code.

[

{

path: 'moduleA',

loadChildren: './moduleA.module',

data: {preload: true}

}

{

path: 'moduleB',

loadChildren: './moduleB.module'

}

]

In this case, we need to view the strategy and has to implement this pre-loading strategy interface. The only method required here is to pre-load the two parameters: the route and the function that actually does the pre-loading and turns an observable. For this strategy, we are checking the pre-load property is set to true. In such cases, the function is called. This strategy is registered now and there are two steps here. First, it is required to provide a list as a provider, and then we can parse it as a token for the route. When it fetches the strategy, it functions well. It is pluggable, so we can customize it.

export class PreloadSelctedModulesList implements PreloadingStrategy {

preload(route: Route, load: Function): Observable<any> {

return route.data.preload ? load(): of (null);

}

}

 Bundling

Creating separate files for each module sounds like a very daunting task. However its pretty much automated in Angular. You can use the CLI tool or create a custom webpack config to get this done quickly and efficiently. If you are using the CLI it will handle everything for you, there is no need for change any configuration.

Conclusion

  • Router is stable
  • It supports transparent lazy loading
  • It supports transparent preloading
  • It supports automatic bundling