1. Why Do We Need Lazy Loading in Angular?
What is the purpose?
We need lazy loading to avoid loading the entire Angular application's feature code when the application first starts.
Instead:
Load the initial features first and load other features only when the user navigates to them.
Consider an enterprise application containing:
Home
Products
Customers
Orders
Reports
Admin
Settings
Audit
A user may only visit:
Home → Products → Orders
There is no reason to immediately load code for:
Reports
Admin
Settings
Audit
Lazy loading allows those features to be downloaded when they are actually required.
Without Lazy Loading
Suppose the application contains several features:
Application
│
├── Home
├── Products
├── Orders
├── Reports
└── Admin
If everything is eagerly included:
Browser
↓
Application starts
↓
Download initial application code
↓
Home
Products
Orders
Reports
Admin
↓
Application ready
As an application grows, this can increase the amount of JavaScript the browser initially downloads, parses, and executes.
With Lazy Loading
Browser
↓
Application starts
↓
Load core/initial code
↓
Home displayed
Later:
User clicks Products
↓
Products code downloaded
↓
Products displayed
Later:
User clicks Admin
↓
Admin code downloaded
↓
Admin displayed
This is on-demand loading.
Advantages
1. Smaller initial bundle
Less feature code needs to be included in the initial application load.
2. Faster startup
The browser initially has less JavaScript to:
Download
↓
Parse
↓
Compile
↓
Execute
3. Better for large applications
It is particularly useful for large enterprise applications containing many independent features.
4. Avoid loading unused features
If a normal user never visits /admin, the Admin feature doesn't need to be loaded during startup.
5. Better feature separation
Features can be organized independently:
products/
orders/
reports/
admin/
Key Point
Lazy loading mainly improves initial application loading by postponing feature code until that feature is needed.
2. How Was Lazy Loading Implemented with Feature NgModules?
Before standalone Angular became the preferred architecture, Angular applications commonly organized functionality into feature NgModules.
For example:
AppModule
|
+-- Home
|
+-- ProductsModule
|
+-- AdminModule
Instead of importing ProductsModule directly into AppModule, we configured the router to load it using:
loadChildren
Full Simple Sample Program
Consider a traditional NgModule-based application with a lazy-loaded Products feature.
Project Structure
src/app/
│
├── app.module.ts
├── app-routing.module.ts
├── app.component.ts
│
└── products/
│
├── products.module.ts
├── products-routing.module.ts
├── products.component.ts
└── products.component.html
app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<h1>My Application</h1>
<nav>
<a routerLink="/products">Products</a>
</nav>
<hr>
<router-outlet></router-outlet>
`
})
export class AppComponent {
}
app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule
],
providers: [],
bootstrap: [
AppComponent
]
})
export class AppModule {
}
Notice something important:
imports: [
BrowserModule,
AppRoutingModule
]
We do not import ProductsModule here.
For lazy loading, avoid:
import { ProductsModule } from './products/products.module';
@NgModule({
imports: [
BrowserModule,
ProductsModule, // ❌ would eagerly import the feature
AppRoutingModule
]
})
Instead, the router loads it dynamically.
app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
const routes: Routes = [
{
path: 'products',
loadChildren: () =>
import('./products/products.module')
.then(m => m.ProductsModule)
}
];
@NgModule({
imports: [
RouterModule.forRoot(routes)
],
exports: [
RouterModule
]
})
export class AppRoutingModule {
}
This is the most important part:
loadChildren: () =>
import('./products/products.module')
.then(m => m.ProductsModule)
products/products.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ProductsComponent } from './products.component';
import { ProductsRoutingModule } from './products-routing.module';
@NgModule({
declarations: [
ProductsComponent
],
imports: [
CommonModule,
ProductsRoutingModule
]
})
export class ProductsModule {
}
products/products-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ProductsComponent } from './products.component';
const routes: Routes = [
{
path: '',
component: ProductsComponent
}
];
@NgModule({
imports: [
RouterModule.forChild(routes)
],
exports: [
RouterModule
]
})
export class ProductsRoutingModule {
}
products/products.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-products',
templateUrl: './products.component.html'
})
export class ProductsComponent {
products = [
'Laptop',
'Keyboard',
'Mouse'
];
}
products/products.component.html
<h2>Products</h2>
<ul>
<li *ngFor="let product of products">
{{ product }}
</li>
</ul>
How Does It Work?
When the application starts:
Application starts
↓
AppModule
↓
AppRoutingModule
↓
ProductsModule NOT loaded yet
The router knows:
{
path: 'products',
loadChildren: () =>
import('./products/products.module')
.then(m => m.ProductsModule)
}
But it doesn't immediately load ProductsModule.
When the user navigates to:
/products
Angular performs:
User → /products
↓
Angular Router
↓
Finds loadChildren()
↓
Dynamic import()
↓
Loads ProductsModule
↓
Loads ProductsRoutingModule
↓
Matches path: ''
↓
Creates ProductsComponent
↓
Products page displayed
Understanding forRoot() and forChild()
Traditional routing commonly used these two methods.
At application level:
RouterModule.forRoot(routes)
For example:
@NgModule({
imports: [
RouterModule.forRoot(routes)
]
})
export class AppRoutingModule {
}
This configures the application's root router.
Inside a feature module:
RouterModule.forChild(routes)
For example:
@NgModule({
imports: [
RouterModule.forChild(routes)
]
})
export class ProductsRoutingModule {
}
This adds the feature's child routes to the existing router configuration.
A useful way to remember it:
AppRoutingModule
↓
forRoot()
ProductsRoutingModule
↓
forChild()
Old NgModule vs Modern Standalone Lazy Loading
This is an important interview comparison.
Traditional NgModule
{
path: 'products',
loadChildren: () =>
import('./products/products.module')
.then(m => m.ProductsModule)
}
Flow:
/products
↓
ProductsModule
↓
ProductsRoutingModule
↓
ProductsComponent
Modern Standalone
If Products is a standalone component:
{
path: 'products',
loadComponent: () =>
import('./products/products')
.then(m => m.Products)
}
Flow:
/products
↓
Products standalone component
No feature NgModule is required.
loadChildren() vs loadComponent()
| Feature | Traditional NgModule | Modern Standalone |
|---|---|---|
| Common approach | loadChildren() |
loadComponent() / loadChildren() |
| Loads | Feature module | Component or route collection |
| NgModule required | Yes for module-based pattern | No |
| Example | ProductsModule |
Products |
| Boilerplate | More | Less |
Modern Angular can also use loadChildren() to lazy-load a collection of standalone routes, so loadChildren() itself is not obsolete.
Key Points
- Lazy loading loads feature code on demand.
- Its main purpose is to reduce the amount of JavaScript required during initial application startup.
- Traditional Angular commonly lazy-loaded feature NgModules.
- Feature modules were loaded using:
loadChildren()
- The feature module should not also be eagerly imported into
AppModule. - Traditional root routing commonly used:
RouterModule.forRoot()
- Feature routing commonly used:
RouterModule.forChild()
- Modern standalone Angular can directly lazy-load a component using:
loadComponent()
- Modern standalone route collections can still be lazy-loaded with:
loadChildren()
Interview Questions and Answers
1. Why do we need lazy loading?
Lazy loading reduces the amount of feature code required during initial application startup by loading features only when the user navigates to them. This can improve startup performance, particularly for large applications.
2. How was lazy loading implemented with feature NgModules?
Traditionally, Angular used loadChildren() in the route configuration to dynamically import a feature NgModule.
loadChildren: () =>
import('./products/products.module')
.then(m => m.ProductsModule)
3. Should a lazy-loaded module be imported into AppModule?
Normally, no. If you directly import the feature module into AppModule, it becomes part of the eager dependency graph, defeating the intended lazy-loading boundary.
4. What is RouterModule.forRoot()?
forRoot() configures the root-level Angular router and is normally called once in the application's root routing module.
5. What is RouterModule.forChild()?
forChild() registers additional routes for a feature module without creating another root router.
6. How is modern standalone lazy loading different?
Modern Angular can lazy-load a standalone component directly:
loadComponent: () =>
import('./products/products')
.then(m => m.Products)
Therefore, a ProductsModule and ProductsRoutingModule are not required just to make the Products component lazy-loaded.