January 02, 2017

Optimizing Page Speeds With Lazyloading

by Thomas Greco

angular2-lazyloading

So far, we've covered the basics of Angular 2. In our first tutorial, we learned about the core concepts of the framework, namely modules and components. Following this, in our second article, we explored the @angular/router library by taking a look at how we can route to different components in the framework. Now, we're going to wrap up this set of three articles with a look at how to optimize Angular 2 applications. Specifically, we're going to explore the topic of lazy-loading Angular 2 modules, and learn how we can set up our application to be as highly performant as possible.

Lazy Loading Modules

In part two of this series, we focused on Angular's ability to route specific components to specific URLs. The final result is the plnkr shown below. In this example, each of our routes (or routable components) are loaded when the application is instantiated.

This sample application only contains small bits of dummy code, therefore we are not subjected to any performance penalty due to our entire application being loaded at once. However, as applications grow larger and more complex, users will want to take advantage of the framework's ability to easily perform lazy-load. In simplest terms, this tells our application to refrain from loading certain sections of our application until they are needed.

When working with Angular 2, we can program the application to lazily-load specific modules. In the first article, I spoke about the use of modules, and briefly mentioned the structure of large Angular 2 applications. Additionally, I introduced the idea of feature modules, which are Angular modules that hold the code for components, directives, services, etc. inside of a specific module.
An example could be a @NgModule decorator that holds the code for the blog section of a website, BlogrollComponent and SinglePostComponent. In addition to feature modules, Angular also recommends the use of shared modules, which contain code that will be shared throughout an application, such as a site's navigation bar or a footer. Having said that, our sample application will expand upon the components introduced in our article on routing. By the end, we will have HomeComponent, AppComponent, and AboutComponent split into individual modules.

Protect Your Angular App

Code

Each feature module is going to be placed inside of its own folder. The structure will replicate the following:

app  
│ _ app.routes.ts
│ _ app.module.ts
│ _ app.component.ts
│ _ // other 
└───home
   │  home.component.ts
   │   home.component.ts
   │   home.routes.ts
└───about
    │   about.routes.ts
    │   about.module.ts
    │   about.component.ts
└───contact
    │   contact.routes.ts
    │   contact.module.ts
    │   contact.component.ts

The final code for this tutorial can be found at this link.

AboutRoutes, ContactRoutes, HomeRoutes

A quick inspection of this code will show us that each of the .module and .routes files for our child modules are generally the same. (Those interested in learning about these files should check articles 1 and 2). We can tie ContactComponent, AboutComponent and HomeComponent to their respective routes inside of the <component>.routes.ts file.
Soon, we will see how we can use loadChildren to load our modules in our main app.routes.ts file. When defining our child routes, we need to pass in an empty string as the path because we will be defining the url in our main appRoutes configuration. In addition to the URLs, another key part of this file is the RouterModule.forChild method at the bottom of our file. The route.ts file for each feature module is a child configuration that will ultimately be nested inside of our main appRoutes config. Whereas appRoutes uses the .forRoot method, each of our nested modules will use forChild to initialize their routes.

// home/home.routes
import {  
    Route,
    RouterModule
} from '@angular/router';
import {  
    HomeComponent
} from './home.component';
export const HomeRoutes: Route[] = [{  
    path: '',
    component: HomeComponent
}];
export default RouterModule.forChild(HomeRoutes);  
//contact/contact.routes
import {  
    Route,
    RouterModule
} from '@angular/router';
import {  
    ContactComponent
} from './contact.component';
export const ContactRoutes: Route[] = [{  
    path: '',
    component: ContactComponent
}];
export default RouterModule.forChild(ContactRoutes);  
//about/about.routes
import {  
    Route,
    RouterModule
} from '@angular/router';
import {  
    AboutComponent
} from './about.component';
export const AboutRoutes: Route[] = [{  
    path: '',
    component: AboutComponent
}];
export default RouterModule.forChild(AboutRoutes);  

AboutModule, ContactModule, HomeModule

The only differences between HomeModule, AboutModule and ContactModule are in the form of file names and imports. Each of them has their respective routes being imported from the directory's routes.ts file in addition to the CommonModule, which gives our templates access to the framework's common directives like ngIf, ngFor. (the NgModule decorator metadata is described in detail in article 1). Following our imports, we pass in the name of any component tied to a module into the declarations property. By doing this, we are making Angular aware of the component. When routing to components directly, we passed in the name of our components to the declarations property inside of our main AppModule. By declaring our component directly inside of our module, Angular will be aware of it through the feature module, so we will no longer have these declarations inside of ourAppModule.

//home/home.module
import {  
    NgModule
} from '@angular/core';
import {  
    HomeComponent
} from './home.component';
import {  
    CommonModule
} from '@angular/common';
import homeRoutes from './home.routes';  
@NgModule({
    imports: [
        CommonModule,
        homeRoutes
    ],
    declarations: [HomeComponent]
})
export class HomeModule {}  
//contact/contact.module
import {  
    NgModule
} from '@angular/core';
import {  
    ContactComponent
} from './contact.component';
import {  
    CommonModule
} from '@angular/common';
import routes from './contact.routes';  
@NgModule({
    imports: [
        CommonModule,
        routes
    ],
    declarations: [ContactComponent]
})
export class ContactModule {}  
//about/about.module
import {  
    NgModule
} from '@angular/core';
import {  
    AboutComponent
} from './about.component';
import {  
    CommonModule
} from '@angular/common';
import aboutRoutes from './about.routes';

@NgModule({
    imports: [
        CommonModule,
        aboutRoutes
    ],
    declarations: [AboutComponent]
})
export class AboutModule {}  

Loading our modules.

When working with routable components, each route definition is tied directly to a component. We see this inside of our <componentName.routes.ts> files.

// src/contact/contact.routes.ts
export const ContactRoutes: Route[] = [  
  { path: '', component: ContactComponent }
];
// src/about/about.routes.ts
export cons AboutRoutes: Route[] = [  
  { path: '', component: AboutComponent }
];
// src/home/home.routes.ts
export const HomeRoutes: Route[] = [  
  { path: '', component: HomeComponent }
];

In addition to our child routes, we need to configure Angular’s RouterModule to load our module inside of app.routes.ts. By taking a look at ContactRoutes, HomeRoutes, and AboutRoutes configurations above, we see that the path value of each component is an empty string. This allows us to assign a URL path for each of the code tied to this module directly inside of our main app.routes.ts file. Below, we can see our root appRoutes config which contains the route definitions for our app.

//src/app.routes.ts
import {  
    Routes,
    RouterModule
} from '@angular/router';
export const routes: Routes = [{  
        path: '',
        loadChildren: './src/home/home.module#HomeModule'
    },
    {
        path: 'contact',
        loadChildren: './src//contact/contact.module#ContactModule'
    },
    {
        path: 'about',
        loadChildren: './src//about/about.module#AboutModule'
    }
];

As we learned before, our modules are tied to empty strings, so we assign our desired paths of each module here. Our example has the following URLs : / /contact and /about. Further examination of our Routes config shows a special string being passed into the loadChildren property. When mapping a path to a module we use the # symbol to tell Angular where our module lies. It's important to note that Angular 2 will not render our code directly if this string format is not used correctly. Below, we can see the app.routes.ts file for this example compared to the app.routes.ts file in the tutorial on routing to components.

Lazy Loaded Routes vs. Component Routes

Below, we can see the differences from the routes used in our first tutorial and this application's routes. In this code, we see that our lazy-loaded routes uses the loadChildren property spoken about above; meanwhile the config found in our initial application ties our views to specific components via the component property.

// app.routes.ts (Lazy-loading)
import {  
    Routes,
    RouterModule
} from '@angular/router';
export const routes: Routes = [{  
        path: '',
        loadChildren: './src/home/home.module#HomeModule'
    },
    {
        path: 'contact',
        loadChildren: './src/contact/contact.module#ContactModule'
    },
    {
        path: 'about',
        loadChildren: './src/about/about.module#AboutModule'
    }
];
export default RouterModule.forRoot(routes);  
//app.routes.ts (Component Router)
/* Import Routes Config */
import {  
    RouterModule
} from '@angular/router';
/* Import Individual Component */
import {  
    HomeComponent
} from './home.component';
import {  
    AboutComponent
} from './about.component';
import {  
    ContactComponent
} from './contact.component';
const routes = [{  
        path: '',
        component: HomeComponent
    },
    {
        path: 'about',
        component: AboutComponent
    },
    {
        path: 'contact',
        component: ContactComponent
    }
];
export default RouterModule.forRoot(routes);  

Conclusion

And this concludes our article on lazy-loading in Angular 2. So far, we've learned a good bit about the Angular 2 framework.

The ability to perform lazy-loading is a highly sought one, knowledge of how to configure lazily-loaded routes is very useful. Victor Savkin states in this article that some program applications only load certain modules when needed instead of loading all of our code at once. We definitely recommend checking out his article to anyone interested in learning more about the topic!

Protect Your Angular App