Lazy Loading: NgModules vs Standalone Components
The main difference is:
Older NgModule approach
↓
Lazy load a Feature Module
↓
loadChildren()
Modern Standalone approach
↓
Lazy load a Component
↓
loadComponent()
OR
Lazy load a group of Routes
↓
loadChildren()
1. How Was Lazy Loading Implemented with NgModules?
What is it?
Before standalone components, Angular applications were commonly organized using NgModules.
For a large feature such as Products, we might have:
AppModule
│
└── ProductsModule
├── ProductListComponent
├── ProductDetailsComponent
└── ProductsRoutingModule
Instead of loading ProductsModule when the application starts, Angular could lazy load it when the user navigated to:
/products
This was configured using:
loadChildren()
Full Simple Sample Program
Project structure
app/
│
├── app.module.ts
├── app-routing.module.ts
│
└── products/
├── products.module.ts
├── products-routing.module.ts
├── product-list.component.ts
└── product-details.component.ts
product-list.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-product-list',
template: `
<h2>Product List</h2>
<ul>
<li>Laptop</li>
<li>Mobile</li>
<li>Keyboard</li>
</ul>
`
})
export class ProductListComponent {
}
product-details.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-product-details',
template: `
<h2>Product Details</h2>
`
})
export class ProductDetailsComponent {
}
products-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ProductListComponent } from './product-list.component';
import { ProductDetailsComponent } from './product-details.component';
const routes: Routes = [
{
path: '',
component: ProductListComponent
},
{
path: ':id',
component: ProductDetailsComponent
}
];
@NgModule({
imports: [
RouterModule.forChild(routes)
],
exports: [
RouterModule
]
})
export class ProductsRoutingModule {
}
products.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ProductListComponent } from './product-list.component';
import { ProductDetailsComponent } from './product-details.component';
import { ProductsRoutingModule } from './products-routing.module';
@NgModule({
declarations: [
ProductListComponent,
ProductDetailsComponent
],
imports: [
CommonModule,
ProductsRoutingModule
]
})
export class ProductsModule {
}
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 {
}
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
],
bootstrap: [
AppComponent
]
})
export class AppModule {
}
Explanation
The most important code is:
{
path: 'products',
loadChildren: () =>
import('./products/products.module')
.then(m => m.ProductsModule)
}
Notice that ProductsModule is not statically imported into AppModule.
When the user navigates to:
/products
the flow is:
User visits /products
↓
AppRoutingModule
↓
loadChildren()
↓
import ProductsModule
↓
ProductsModule
↓
ProductsRoutingModule
↓
RouterModule.forChild(routes)
↓
ProductListComponent
For:
/products/10
the feature route:
{
path: ':id',
component: ProductDetailsComponent
}
matches and displays ProductDetailsComponent.
Important
If you wanted the module to remain lazy loaded, you would not also add:
imports: [
ProductsModule
]
to AppModule.
Doing so would make the feature part of the eager application dependency graph.
2. How Is Lazy Loading Implemented with Standalone Components?
What is it?
With standalone Angular, we don't need to create an NgModule simply to organize and lazy load components.
There are two important approaches:
Single Component
↓
loadComponent()
Multiple Feature Routes
↓
loadChildren()
Approach 1 — loadComponent()
Use loadComponent() when you want to lazy load an individual standalone component.
Full Simple Example
products.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-products',
standalone: true,
template: `
<h2>Products</h2>
<ul>
<li>Laptop</li>
<li>Mobile</li>
<li>Keyboard</li>
</ul>
`
})
export class Products {
}
app.routes.ts
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: 'products',
loadComponent: () =>
import('./products')
.then(m => m.Products)
}
];
app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes)
]
};
When the user visits:
/products
Angular executes:
import('./products')
and loads the Products component.
Flow:
/products
↓
Angular Router
↓
loadComponent()
↓
Dynamic import
↓
Products
↓
<router-outlet>
Approach 2 — loadChildren() with Standalone Routes
If a feature contains multiple pages:
/products
/products/add
/products/10
we can place those routes in a separate route file.
Full Simple Example
product-list.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-product-list',
standalone: true,
template: `
<h2>Product List</h2>
`
})
export class ProductList {
}
add-product.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-add-product',
standalone: true,
template: `
<h2>Add Product</h2>
`
})
export class AddProduct {
}
product-details.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-product-details',
standalone: true,
template: `
<h2>Product Details</h2>
`
})
export class ProductDetails {
}
products.routes.ts
import { Routes } from '@angular/router';
import { ProductList } from './product-list';
import { AddProduct } from './add-product';
import { ProductDetails } from './product-details';
export const PRODUCT_ROUTES: Routes = [
{
path: '',
component: ProductList
},
{
path: 'add',
component: AddProduct
},
{
path: ':id',
component: ProductDetails
}
];
app.routes.ts
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: 'products',
loadChildren: () =>
import('./products/products.routes')
.then(m => m.PRODUCT_ROUTES)
}
];
Now:
/products
↓
ProductList
/products/add
↓
AddProduct
/products/10
↓
ProductDetails
The Products route configuration itself is loaded lazily.
NgModule vs Standalone Lazy Loading
| NgModule Approach | Standalone Approach |
|---|---|
| Lazy load feature module | Lazy load component or routes |
loadChildren() |
loadComponent() / loadChildren() |
ProductsModule required |
No feature NgModule required |
ProductsRoutingModule common |
products.routes.ts common |
RouterModule.forChild() |
Plain Routes array |
RouterModule.forRoot() |
provideRouter() |
Side-by-Side Code
Older NgModule
{
path: 'products',
loadChildren: () =>
import('./products/products.module')
.then(m => m.ProductsModule)
}
This loads:
ProductsModule
↓
ProductsRoutingModule
↓
RouterModule.forChild()
↓
Components
Modern Standalone — Single Component
{
path: 'products',
loadComponent: () =>
import('./products')
.then(m => m.Products)
}
This loads:
Products Component
Modern Standalone — Feature Routes
{
path: 'products',
loadChildren: () =>
import('./products/products.routes')
.then(m => m.PRODUCT_ROUTES)
}
This loads:
PRODUCT_ROUTES
↓
├── ProductList
├── AddProduct
└── ProductDetails
One Important Improvement
In the previous standalone loadChildren() example, products.routes.ts statically imports its components:
import { ProductList } from './product-list';
import { AddProduct } from './add-product';
So once PRODUCT_ROUTES is loaded, those component dependencies belong to that lazy feature chunk/dependency graph.
You can go further and lazy load the individual components too:
import { Routes } from '@angular/router';
export const PRODUCT_ROUTES: Routes = [
{
path: '',
loadComponent: () =>
import('./product-list')
.then(m => m.ProductList)
},
{
path: 'add',
loadComponent: () =>
import('./add-product')
.then(m => m.AddProduct)
},
{
path: ':id',
loadComponent: () =>
import('./product-details')
.then(m => m.ProductDetails)
}
];
Conceptually:
Application starts
↓
Products routes not loaded
User visits /products
↓
PRODUCT_ROUTES loaded
↓
ProductList loaded
User later visits /products/add
↓
AddProduct loaded
This gives you more granular lazy loading.
Key Points
- In traditional NgModule applications, lazy loading commonly used:
loadChildren()
to load a feature module.
- Example:
loadChildren: () =>
import('./products/products.module')
.then(m => m.ProductsModule)
- The lazy feature commonly had:
ProductsModule
ProductsRoutingModule
RouterModule.forChild()
- With standalone Angular, an individual component can be lazy loaded using:
loadComponent()
- Example:
loadComponent: () =>
import('./products')
.then(m => m.Products)
- Multiple feature routes can be lazy loaded using:
loadChildren()
with a Routes array.
- Standalone architecture removes the requirement to create an NgModule merely for routing/lazy-loading organization.
Interview Questions and Answers
1. How was lazy loading implemented with NgModules?
Feature modules were commonly lazy loaded using loadChildren():
{
path: 'products',
loadChildren: () =>
import('./products/products.module')
.then(m => m.ProductsModule)
}
The feature module typically contained a routing module using RouterModule.forChild().
2. How is a standalone component lazy loaded?
Using loadComponent():
{
path: 'products',
loadComponent: () =>
import('./products')
.then(m => m.Products)
}
3. Can loadChildren() be used with standalone Angular?
Yes. It can lazy load a route configuration:
{
path: 'products',
loadChildren: () =>
import('./products/products.routes')
.then(m => m.PRODUCT_ROUTES)
}
No feature NgModule is required.
4. What is the major difference?
NgModule approach
↓
loadChildren()
↓
ProductsModule
Standalone approach
↓
loadComponent()
↓
Standalone Component
OR
loadChildren()
↓
Standalone Routes
5. Which should you use for a single standalone page?
Usually:
loadComponent()
Example:
{
path: 'about',
loadComponent: () =>
import('./about')
.then(m => m.About)
}
6. Which should you use for a feature containing multiple routes?
A separate route configuration with loadChildren() is often a clean choice:
{
path: 'products',
loadChildren: () =>
import('./products/products.routes')
.then(m => m.PRODUCT_ROUTES)
}
Interview-ready answer
In NgModule-based Angular, lazy loading was commonly implemented with
loadChildren()to dynamically load a feature module, whose routing was usually configured withRouterModule.forChild(). In standalone Angular, we can lazy load an individual standalone component usingloadComponent(), or lazy load an entire feature's route configuration usingloadChildren(). This removes the need to create feature NgModules solely for routing and lazy loading.