Lazy Loading Routes
As an application grows, bundling every feature into the initial JavaScript payload slows down the first paint and time-to-interactive. Lazy loading defers the download of a route’s code until a user actually navigates to it, keeping the initial bundle small and the app fast. In modern, standalone-first Angular this is wonderfully simple: you point a route at a dynamic import() using loadComponent for a single page or loadChildren for an entire feature area, and Angular’s router and the build tooling handle the code-splitting for you.
Why lazy loading matters
Every route you reference eagerly with the component property is pulled into the main chunk at build time. For a dashboard, an admin area, or a settings screen that most users rarely open, that is wasted bandwidth on every visit. Lazy loading turns each feature into its own chunk that the browser fetches on demand, which directly improves Core Web Vitals like Largest Contentful Paint.
The router supports three loading strategies that map cleanly onto how your routes are shaped:
| Property | Loads | Use when |
|---|---|---|
component | A standalone component, eagerly | Small, always-needed routes |
loadComponent | A single standalone component, lazily | One self-contained page |
loadChildren | A set of child routes, lazily | A whole feature area |
Lazy loading a single component
loadComponent takes a function returning a dynamic import() that resolves to a standalone component class. Because the import is only evaluated when the route activates, Angular’s build (esbuild via the Vite-based dev server) emits the component and its private dependencies as a separate chunk.
// app.routes.ts
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: '',
loadComponent: () =>
import('./home/home.component').then((m) => m.HomeComponent),
},
{
path: 'reports',
loadComponent: () => import('./reports/reports.component'),
},
];
If the target file uses a default export, you can return the promise directly without .then(), as shown for the reports route above. Prefer default exports for lazy components to keep route files terse.
// reports/reports.component.ts
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-reports',
standalone: true,
template: `
<h1>Reports</h1>
@if (loading()) {
<p>Crunching numbers…</p>
} @else {
<p>{{ count() }} reports ready.</p>
}
`,
})
export default class ReportsComponent {
protected readonly loading = signal(false);
protected readonly count = signal(42);
}
Lazy loading a feature area with loadChildren
When a feature has several routes — say an admin section with a list, a detail page, and settings — group them with loadChildren. The dynamic import resolves to an array of child Routes, all bundled into one chunk and shared across the feature.
// app.routes.ts
export const routes: Routes = [
{
path: 'admin',
loadChildren: () => import('./admin/admin.routes').then((m) => m.ADMIN_ROUTES),
},
];
// admin/admin.routes.ts
import { Routes } from '@angular/router';
export const ADMIN_ROUTES: Routes = [
{
path: '',
loadComponent: () => import('./admin-shell.component'),
children: [
{
path: 'users',
loadComponent: () => import('./users/user-list.component'),
},
{
path: 'users/:id',
loadComponent: () => import('./users/user-detail.component'),
},
{ path: '', redirectTo: 'users', pathMatch: 'full' },
],
},
];
The child route paths are relative to the parent admin segment, so /admin/users/7 activates UserDetailComponent. Combining loadChildren for the area with loadComponent per page gives you fine-grained chunks without manual configuration.
Tip: You can attach route-level providers to a lazy area using the
providersarray on the parent route. Those providers become available to every child component and are torn down when you navigate away, giving you a clean, feature-scoped injector.
Verifying the split
After building, each lazy route appears as its own file. Run the production build and inspect the output.
ng build
Output:
Initial chunk files | Names | Raw size
main-7QH4XK2A.js | main | 142.81 kB
polyfills-...js | polyfills | 34.58 kB
Lazy chunk files | Names | Raw size
chunk-RT5KL9PZ.js | admin-routes | 18.40 kB
chunk-9F2BQ1WC.js | reports | 6.12 kB
Preloading for snappier navigation
Lazy loading trades a smaller first load for a brief fetch when the user navigates. To hide that latency you can preload lazy chunks in the background after the app is interactive. Enable a preloading strategy in provideRouter.
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter, withPreloading, PreloadAllModules } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes, withPreloading(PreloadAllModules))],
};
PreloadAllModules fetches every lazy chunk once the initial navigation settles. For finer control, implement a custom PreloadingStrategy that only preloads routes flagged with data: { preload: true }.
Best practices
- Default to
loadComponent/loadChildrenfor any route a user might not need immediately; reserve eagercomponentfor the landing route and shared shells. - Use
defaultexports on lazily loaded components so route files stay clean and the import returns the class directly. - Group cohesive features behind one
loadChildrenboundary so shared dependencies are deduplicated into a single chunk. - Scope feature services with route-level
providersinstead ofprovidedIn: 'root'when they belong only to a lazy area. - Add a preloading strategy (or a custom one) to mask the fetch latency on likely next destinations.
- Run
ng buildand review the lazy chunk list to confirm features are actually code-split and no chunk has ballooned.