A short introduction
The main focus of this article is to explore options for optimizing load times in larger front end applications. We'll also touch on how route guards can be combined with lazy loading module to provide additional security.
Eager loading
Eager loading is Angular's default loading strategy. All eagerly loaded components and modules are loaded before the application is started, and so, this has a detrimental impact on our application start up time.
It's important to consider user journeys and which common tasks need to be eagerly loaded to keep our applications robust and quick.
Let's create some eagerly loaded components together. Begin by creating a new Angular application:
$ng new loading-demo
Navigate to the app.routing module ./src/app/app-routing.module.ts
and we'll create some routes for our future home and not found pages.
import { NgModule } from "@angular/core";
import { Routes, RouterModule } from "@angular/router";
// These will error as they don't exist yet.
const routes: Routes = [
{ path: "", component: HomePageComponent },
{ path: "not-found", component: NotFoundComponent },
{ path: "**", redirectTo: "/not-found" },
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}
At this point the your might be wondering why we created our routes before our components exist 🤔. We can use some CLI options to scaffold our components within a module using the --module Angular command line option.
Let's create our home and not found page using the Angular CLI, we'll opt to declare these in our app-routing module:
$ng g c home-page --module app-routing.module
CREATE src/app/home-page/home-page.component.html (24 bytes)
CREATE src/app/home-page/home-page.component.spec.ts (643 bytes)
CREATE src/app/home-page/home-page.component.ts (287 bytes)
CREATE src/app/home-page/home-page.component.scss (0 bytes)
UPDATE src/app/app-routing.module.ts (488 bytes)
$ng g c not-found --module app-routing.module
CREATE src/app/not-found/not-found.component.html (24 bytes)
CREATE src/app/not-found/not-found.component.spec.ts (643 bytes)
CREATE src/app/not-found/not-found.component.ts (287 bytes)
CREATE src/app/not-found/not-found.component.scss (0 bytes)
UPDATE src/app/app-routing.module.ts (576 bytes)
Notice the updates to our app-routing.module.ts. We didn't have to import and declare our components inside our module. We let the CLI do that.
Here's what our app module looks like afterwards:
import { NgModule } from "@angular/core";
import { Routes, RouterModule } from "@angular/router";
import { HomePageComponent } from "./home-page/home-page.component";
import { NotFoundComponent } from "./not-found/not-found.component";
const routes: Routes = [
{ path: "", component: HomePageComponent },
{ path: "not-found", component: NotFoundComponent },
{ path: "**", redirectTo: "/not-found" },
];
not - found;
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
declarations: [HomePageComponent, NotFoundComponent],
})
export class AppRoutingModule {}
💡 TIP - You don't need to create modules before your components. It just makes this article a little bit shorter and easier to read.
As we mentioned before, eager loading is Angular's default loading strategy. So our home and not found pages are eagerly loaded.
Serve the application, when it opens you should see that your home page component works. Once you've confirmed this try navigating to a route that doesn't exist. You should be redirected to the "not-found" route.
$ ng serve -o
If you've used Angular before there should be no major surprises here. We've simly create an application with two eagerly loaded pages.
Lazy loading
The term "lazy loading" describes the concept of loading components and modules at runtime, as and when they're required.
At this point let's assume that our site has an optional user registration, and login system. Only a few of our visitors use these options so it might be nice to load these parts of the system as and when a user attempts to login, register.
We can encapsulate all of this functionality into a module and then lazy load the module as and when needed.
It's a good idea to break an application's parts into modules based on functionality, having each feature self-contained within a module.
This will help to keep your code neat and well structured. It also gives us the ability to load in the "user-signin" feature when a specific domain sub-directory is accessed (e.g. http://yoursite.com/user-signin/...)
Let's begin by creating a module for the user content feature:
$ ng g m user-signin --routing
CREATE src/app/user-signin/user-signin-routing.module.ts (255 bytes)
CREATE src/app/user-signin/user-signin.module.ts (301 bytes)
As you can see this created two files:
- the user-signin.module.ts module
- the user-signin-routing.module.ts module
These are akin to our app.module and app-routing.module files where our user-signin module exports our user signin routing module:
import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
import { UserSignInRoutingModule } from "./user-signin-routing.module";
import { LoginPageComponent } from '../login-page/login-page.component';
import { RegisterPageComponent } from '../register-page/register-page.component';
@NgModule({
declarations: [LoginPageComponent, RegisterPageComponent],
imports: [CommonModule, UserSignInRoutingModule],
})
export class UserSignInModule { }
Our user-signin routing module looks like this:
import { NgModule } from "@angular/core";
import { Routes, RouterModule } from "@angular/router";
const routes: Routes = [];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class UserSignInRoutingModule {}
Let's define some routes for our components. We'll then generate our components and add them to our module simultaneously as we did before.
import { NgModule } from "@angular/core";
import { Routes, RouterModule } from "@angular/router";
const routes: Routes = [
{ path: "login", component: LoginPageComponent },
{ path: "register", component: RegisterPageComponent },
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class UserSignInRoutingModule {}
And we'll create page components for this module:
$ng g c login-page --module user-signin-routing.module
CREATE src/app/login-page/login-page.component.html (25 bytes)
CREATE src/app/login-page/login-page.component.spec.ts (650 bytes)
CREATE src/app/login-page/login-page.component.ts (291 bytes)
CREATE src/app/login-page/login-page.component.scss (0 bytes)
UPDATE src/app/user-signin/user-signin-routing.module.ts (379 bytes)
ng g c register-page --module user-signin/user-signin-routing.module
CREATE src/app/register-page/register-page.component.html (27 bytes)
CREATE src/app/register-page/register-page.component.spec.ts (664 bytes)
CREATE src/app/register-page/register-page.component.ts (299 bytes)
CREATE src/app/register-page/register-page.component.scss (0 bytes)
UPDATE src/app/user-signin/user-signin-routing.module.ts (480 bytes)
Now our user-signin-routing.module.ts should look like this:
import { NgModule } from "@angular/core";
import { Routes, RouterModule } from "@angular/router";
const routes: Routes = [
{ path: "login", component: LoginPageComponent },
{ path: "register", component: RegisterPageComponent },
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class UserSignInRoutingModule {}
We now need to go back to our app routing module and define a route for all of our user signin (i.e our user-signin module). As before we add a path to the routes collection however the signature is a little different this time:
{
path: "user-signin",
loadChildren: () =>
import("./user-signin/user-signin.module").then(
(m) => m.UserSignInModule
),
},
As you can see this route loads children "on the fly" by dyanamically importing the module when the route is accessed.
Proving It Works
Don't believe me? Why should you? I wouldn't believe me either. Seeing is believing, as they say.
We need a way to observe that this is working. By opening the browsers dev tools and clicking on the network tab you can see which parts of your site are loading. Now navigate to /user-signin/login
Notice that your browser only loads in the module when navigating to the /user-signin route.
Later we'll revisit Lazy loading and we'll implement it in conjunction with route guards. To prevent modules loading when users don't have basic access.
Preloading
In contrast to lazy loading, preloading occurs immediately after eagerly loaded components have initialised and the application is started.
Preloading components requires the use of a strategy. Angular has a built-in PreloadAllModules strategy that simply pre-loads all modules defined within a router configuration.
Fine-grained control of preloading can be achieved using custom preloading strategies. This enables you to conditionally pre-load modules based on our own conditional logic.
Lazy Loading & Route Guards
Imagine for a moment that we have a new requirement to have a profile page for logged in users.
We don't want to lazily load this route until we can validate that the user has been authenticated. If the user navigates to the profile route prior to authenticating we may want to redirect them to the login page.
Let's take a look at how we can implement this in our application. First we need a module for all guarded components. We'll then add our profile component to this newly created module. Now that we know what we're doing we can go this on a single line.
> ng g m auth-guarded --routing; ng g c profile --module auth-guarded/auth-guarded.module.ts
CREATE src/app/auth-guarded/auth-guarded-routing.module.ts (255 bytes)
CREATE src/app/auth-guarded/auth-guarded.module.ts (301 bytes)
CREATE src/app/profile/profile.component.html (22 bytes)
CREATE src/app/profile/profile.component.spec.ts (635 bytes)
CREATE src/app/profile/profile.component.ts (280 bytes)
CREATE src/app/profile/profile.component.scss (0 bytes)
UPDATE src/app/auth-guarded/auth-guarded.module.ts (382 bytes)
note that I'm using powershell, if you're using bash you can simply use
to combine your two commands.
add a route for the profile component in the auth-guarded-routing.module file as we've done before:
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { ProfileComponent } from '../profile/profile.component';
const routes: Routes = [
{
path: "profile",
component: ProfileComponent
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AuthGuardedRoutingModule { }
Then add this module to our app.routing.module as we've done for the other components:
import { NgModule } from "@angular/core";
import { Routes, RouterModule } from "@angular/router";
import { HomePageComponent } from "./home-page/home-page.component";
import { NotFoundComponent } from "./not-found/not-found.component";
const routes: Routes = [
{ path: "", component: HomePageComponent },
{ path: "404", component: NotFoundComponent },
{
path: "user-signin",
loadChildren: () =>
import("./user-signin/user-signin.module").then(
(m) => m.UserSignInModule
),
},
{
path: "auth-guarded",
loadChildren: () =>
import("./auth-guarded/auth-guarded.module").then(
(m) => m.AuthGuardedModule
),
},
{ path: "**", redirectTo: "/404" },
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
declarations: [HomePageComponent, NotFoundComponent],
})
export class AppRoutingModule { }
At this point, I think our routes are looking a little bit ugly. Let's rename them to /authentication and /user. In the real world, we should probably refactor the modules too but I don't think we need this to do this for the purposes of this document.
import { NgModule } from "@angular/core";
import { Routes, RouterModule } from "@angular/router";
import { HomePageComponent } from "./home-page/home-page.component";
import { NotFoundComponent } from "./not-found/not-found.component";
const routes: Routes = [
{ path: "", component: HomePageComponent },
{ path: "404", component: NotFoundComponent },
{
path: "authentication",
loadChildren: () =>
import("./user-signin/user-signin.module").then(
(m) => m.UserSignInModule
),
},
{
path: "user",
loadChildren: () =>
import("./auth-guarded/auth-guarded.module").then(
(m) => m.AuthGuardedModule
),
},
{ path: "**", redirectTo: "/404" },
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
declarations: [HomePageComponent, NotFoundComponent],
})
export class AppRoutingModule { }
Now we need to implement a route guard, route guards have life cycles which in turn use callback functions. These callbacks are defined in different interfaces. For the purposes of loading in the module when authenticated we need to use the CanLoad interface:
> ng g g auth/auth
? Which interfaces would you like to implement? CanLoad
CREATE src/app/auth/auth.guard.spec.ts (331 bytes)
CREATE src/app/auth/auth.guard.ts (410 bytes)
As you can see, this has created the file
.
The contents of the file:
``` typescript
import { Injectable } from '@angular/core';
import { CanLoad, Route, UrlSegment, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from '@angular/router';
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanLoad {
canLoad(
route: Route,
segments: UrlSegment[]): Observable<boolean> | Promise<boolean> | boolean {
return true;
}
}
As you can see we have a canLoad method where we can have some logic to determine whether or not the user is currently logged in. Typically we'd inject a service into this module and use that service provide a flag representing the authentication status.
Let's create a mock service for this now just to prove the point:
> ng g s auth/auth
CREATE src/app/auth/auth.service.spec.ts (347 bytes)
CREATE src/app/auth/auth.service.ts (133 bytes)
Modify the service to give it a property that represents the user's logged-in state:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class AuthService {
public isAuthenticated: boolean = false;
constructor() { }
}
Now we're going to modify our authentication guard to use the service and we'll also use the angular router to redirect the user to the login page if they're not currently logged in:
import { Injectable } from '@angular/core';
import { CanLoad, Route, UrlSegment, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router';
import { AuthService } from './auth.service';
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanLoad {
constructor(private router: Router, private authservice: AuthService) { }
canLoad(route: Route): boolean {
if (this.authservice.isAuthenticated === false) {
this.router.navigateByUrl("/authentication/login");
}
return this.authservice.isAuthenticated;
}
}
Finally we need to hook up our authentication route guard inside of our app routing module like so:
import { NgModule } from "@angular/core";
import { Routes, RouterModule } from "@angular/router";
import { HomePageComponent } from "./home-page/home-page.component";
import { NotFoundComponent } from "./not-found/not-found.component";
import { AuthGuard } from './auth/auth.guard';
const routes: Routes = [
{ path: "", component: HomePageComponent },
{ path: "404", component: NotFoundComponent },
{
path: "authentication",
loadChildren: () =>
import("./user-signin/user-signin.module").then(
(m) => m.UserSignInModule
),
},
{
path: "user",
canLoad: [AuthGuard],
loadChildren: () =>
import("./auth-guarded/auth-guarded.module").then(
(m) => m.AuthGuardedModule
)
,
},
{ path: "**", redirectTo: "/404" },
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
declarations: [HomePageComponent, NotFoundComponent],
})
export class AppRoutingModule { }
Navigate to http://localhost:4200/user/profile and you'll see that the profile module loads.
Now try changing the property in the authentication service to false and you will be redirected to the login page.
💡 There's nothing stopping us from implementing a role-based system where we parts of our application will only load for specific users. If we're allowing users to remain logged in between sessions then we could event these modules to load as part of a preloading strategy but that's beyond the scope of this article.
Top comments (3)
Hello! Interesting article!
Can I ask a question that is not directly related to the article? But it is not to far. I'm trying this since from this article it seems that you know Routing well.
I want in my application,
Angular CLI: 16.1.4
Node: 18.16.1
Package Manager: npm 9.5.1
To have one true source for the whole {route VS menu}. One is mandatory, which is the route itself routers: Route[], the other is optional sidenavMenu: OptionInterface[] which is in side-nav.component.ts and which used by side-nav.component.html to display the menu.
I am thinking of 2 solutions:
To create a global array with objects of the type:
export interface OptionInterface {
name: string;
path: string;
ngxPermissionsOnly: string;
isActive?: boolean;
icon: string;
children?: OptionInterface[];
parentName?: string;
}
and use this object to fill both Routes (which must be adjusted in the time of addition like component, guard, resolver, etc) and sidenavMenu ( are the same)
Use the variable routes: Route[] to fill an array in a service. This service then will be used in side-nav.component.ts. Its looks straightforward, but the problem came when I have to deal with the loadChildren attribute (lazy loading).
Is possible (effectively) one of the solution?
For more info, if you have time you can see my stupid conversation with ChatGPT, about this:
chat.openai.com/share/cbe690fe-0d2...
Hey, sorry for the very late response. I've been super busy at work and I'd not checked this site in a while
I'm not sure I understand the question fully but it sounds like you may want to create modules with their own routes. Then you can import those modules into your app module. It's a but difficult to explain in a comment so I asked the appropriate question to ChatGPT and it wrote a pretty clear response..
chat.openai.com/share/f619d1d8-f5c...
Much easier to follow than anything I could write in this comment box.
No problem! Thank you for the replay!
Thanks to @abdullahmjawaz for responding to the same comment I made on another post . He pointed out an issue in GitHub: github.com/angular/angular/issues/...
I think he had completely gutted my problem, although my comment is not very clear.
In the end, I had to change the solution. As far as I understand, Angular does not have an official way to do this. I had subscribed to the above issue. Let see!
Regards!