| |

Angular 25 🅰️ Lazy Loading Routes

Every byte the browser downloads costs time — parse time, network time, memory. A large Angular app can easily ship megabytes of JavaScript, and loading it all up front delays the first meaningful paint. Lazy loading splits the app into chunks that load on demand. The initial bundle contains only what’s needed to render the first route; everything else waits until the user navigates to it. Angular’s router supports lazy loading at two levels: loadComponent for a single standalone component and loadChildren for a group of child routes. This chapter covers both, plus the bundling behavior, preloading strategies, and the patterns that keep the initial payload small.

Key point: Lazy loading is a routing feature. You mark a route as lazy with loadComponent or loadChildren, and Angular bundles that code into a separate file. The file isn’t fetched until the route activates. The first route stays eager (bundled in main.js), and every lazy route becomes its own chunk. The result: fast initial load, delayed download of the parts users may never visit.


What lazy loading is

Lazy loading defers downloading code until it’s needed.

Eager loading — the default:

import { Routes } from '@angular/router';
import { HomeComponent } from './home.component';
import { AdminComponent } from './admin/admin.component';

export const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'admin', component: AdminComponent }
];

Both components are imported at the top. The bundler includes both in the main bundle. The browser downloads both on initial load, even if the user only visits /.

Lazy loading:

export const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'admin', loadComponent: () => import('./admin/admin.component').then(m => m.AdminComponent) }
];

AdminComponent is loaded via a dynamic import. The bundler puts it in a separate chunk. The browser fetches it only when the user navigates to /admin.

What lazy loading does:

  • Splits the app into chunks
  • Defers download of unused code
  • Reduces the initial bundle
  • Speeds up the first render

What it doesn’t do:

  • Doesn’t reduce total code (all chunks still ship)
  • Doesn’t cache by itself (the browser caches like any file)
  • Doesn’t help if the user visits every route

When it helps: Large apps where users touch a fraction of the routes. Dashboards, admin panels, e-commerce — anywhere the initial view is simple and the rest is deep.

When it doesn’t: Small apps where everything loads fast anyway. Adding lazy loading to a 50 KB app adds complexity for no gain.

Why the router handles it: The router knows when a route activates. That’s exactly the moment the code is needed. Bundling the chunk is a build-time concern; loading it is a runtime one. The router coordinates both through the dynamic import.

Why “lazy”: The code is available but not loaded until asked. It’s lazy — it doesn’t do work until it has to. The name describes the deferral, not the code itself.


loadComponent — lazy-load a standalone component

loadComponent lazy-loads a single standalone component.

export const routes: Routes = [
  {
    path: 'settings',
    loadComponent: () =>
      import('./settings/settings.component').then(m => m.SettingsComponent)
  }
];

The arrow function returns a promise of the component. Angular calls it when the route activates.

What the dynamic import does:

  • import('./path') tells the bundler to create a separate chunk
  • The promise resolves to the module object
  • .then(m => m.SettingsComponent) extracts the component

The signature: loadComponent returns Promise<Type<unknown>> — a promise of a component class.

Multiple lazy components:

export const routes: Routes = [
  { path: '', loadComponent: () => import('./home.component').then(m => m.HomeComponent) },
  { path: 'about', loadComponent: () => import('./about.component').then(m => m.AboutComponent) },
  { path: 'contact', loadComponent: () => import('./contact.component').then(m => m.ContactComponent) }
];

Each route becomes its own chunk. The main bundle contains only the router and the shell.

With route params:

export const routes: Routes = [
  {
    path: 'users/:id',
    loadComponent: () =>
      import('./users/user-detail.component').then(m => m.UserDetailComponent)
  }
];

The component is lazy; the route config is eager. Params work the same — the component reads them via ActivatedRoute.

With guards and resolvers:

export const routes: Routes = [
  {
    path: 'admin',
    loadComponent: () =>
      import('./admin/admin.component').then(m => m.AdminComponent),
    canActivate: [authGuard],
    resolve: { data: adminDataResolver }
  }
];

Guards run before loading the component. If the guard denies access, the chunk isn’t downloaded. That’s a nice property — protected code stays unloaded.

When loadComponent is right:

  • A single standalone component per route
  • Small features that don’t need a whole route group
  • Migrating from module-based lazy loading

Why loadComponent over loadChildren: loadComponent is simpler. Use it when the lazy route is one component. Use loadChildren when the lazy route has its own sub-routes.

Why “component” and not “module”: Standalone components don’t need modules. loadComponent reflects that — it loads just the component. loadChildren remains for cases that need a route group.


loadChildren — lazy-load a route group

loadChildren lazy-loads a set of child routes.

export const routes: Routes = [
  {
    path: 'admin',
    loadChildren: () =>
      import('./admin/admin.routes').then(m => m.adminRoutes)
  }
];

The dynamic import returns a Routes array. Angular merges those routes under /admin.

The admin.routes.ts file:

import { Routes } from '@angular/router';

export const adminRoutes: Routes = [
  { path: '', component: AdminHomeComponent },
  { path: 'users', component: UsersComponent },
  { path: 'settings', component: SettingsComponent }
];

Every route in the array is lazy-loaded together.

URLs work as expected:

  • /adminAdminHomeComponent
  • /admin/usersUsersComponent
  • /admin/settingsSettingsComponent

The parent route’s path (admin) prefixes the children.

loadChildren with loadComponent inside: A lazy route group can use loadComponent for its own routes.

// admin.routes.ts
export const adminRoutes: Routes = [
  { path: '', loadComponent: () => import('./home.component').then(m => m.HomeComponent) },
  { path: 'users', loadComponent: () => import('./users.component').then(m => m.UsersComponent) }
];

That gives you lazy loading at two levels — the group loads as one chunk, and each route within it is its own chunk. Rarely needed, but possible.

Route guards for the whole group:

export const routes: Routes = [
  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.routes').then(m => m.adminRoutes),
    canActivate: [authGuard]
  }
];

The guard runs before the children load. If it fails, the group’s chunk isn’t fetched.

Route data for the group:

{
  path: 'admin',
  loadChildren: () => import('./admin/admin.routes').then(m => m.adminRoutes),
  data: { section: 'admin' }
}

The data is inherited by all children.

When loadChildren is right:

  • A feature with multiple routes
  • An admin section, a shop, a blog
  • Any group of routes that logically loads together

Why loadChildren over loadComponent: When the lazy route has more than one child. One component per route is loadComponent; a route group is loadChildren.

Why “children”: The routes you load become children of the route that loaded them. The parent path is their prefix. The naming reflects the structure — the lazy routes are children of the eager parent.


Legacy loadChildren syntax

Before Angular 15, loadChildren used a string and a # separator.

{
  path: 'admin',
  loadChildren: './admin/admin.module#AdminModule'
}

The string form still works but is deprecated. The new form — a function that returns a promise — is the standard.

The old form:

// Module-based lazy loading
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)

The function form (Angular 15+) is preferred.

Why the change: The string form relied on the bundler parsing a specific syntax. The function form uses standard dynamic import() — clearer, more portable, and works with any bundler.

Migration: Replace the string with a function.

// Before
loadChildren: './admin/admin.module#AdminModule'

// After
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)

Why you’ll see the old form: Older codebases and tutorials use it. New code should use the function form.

Why the function form wins: It’s a standard JavaScript feature. The bundler recognizes import() and creates a chunk. The string form was Angular-specific — it required the bundler to understand Angular’s lazy-loading syntax. Function form works with webpack, Vite, esbuild — any bundler.


Bundling behavior

The bundler sees import() calls and creates chunks.

What you’ll see in the build output:

Initial chunk files     | Names          | Raw size
main.js                 | main           | 250.00 kB
polyfills.js            | polyfills      | 100.00 kB
styles.css              | styles         | 5.00 kB

Lazy chunk files        | Names          | Raw size
chunk-ABC123.js         | admin-routes   | 80.00 kB
chunk-DEF456.js         | settings-comp  | 30.00 kB

The initial chunks download on first load. The lazy chunks download when their routes activate.

What determines a chunk:

  • One import() call → one chunk
  • Imports within a chunk are bundled together
  • Shared code may be extracted into a common chunk

Shared code: If two lazy chunks both import Lodash, the bundler may extract Lodash into a shared chunk. Both lazy chunks reference it.

Common chunk patterns:

PatternBundler action
Two lazy chunks share codeExtract common chunk
Lazy chunk imports eager codeReference the eager code
Eager imports lazy codeUndo lazy loading (bad)

The “undo lazy loading” trap:

// admin.routes.ts (lazy)
import { SharedThing } from '../shared';

// app.routes.ts (eager)
import { AdminComponent } from './admin/admin.component';  // ❌ eager

export const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'admin', loadChildren: () => import('./admin/admin.routes').then(m => m.adminRoutes) }
];

The import { AdminComponent } at the top of app.routes.ts pulls AdminComponent into the eager bundle. Now it’s loaded on first load, defeating the purpose.

The fix: Remove the eager import. Only the lazy import() should reference the lazy code.

// No top-level import of AdminComponent
export const routes: Routes = [
  { path: 'admin', loadChildren: () => import('./admin/admin.routes').then(m => m.adminRoutes) }
];

How to check: Run ng build and look at the chunk sizes. If a “lazy” chunk is small but the main bundle is large, an eager import is pulling code into the main bundle.

Why this matters: Bundling is where lazy loading actually happens. The router loads chunks at runtime, but the split is a build-time decision. If the build doesn’t split correctly, the runtime has nothing to defer.

Why the bundler sees imports: import() is a standard JavaScript syntax that bundlers recognize as a chunk boundary. Static import statements are part of the eager graph. Dynamic import() creates a new graph. The bundler treats each dynamic import as a separate entry point — one chunk.


Preloading strategies

Lazy loading delays the download until the route activates. Preloading downloads the chunks in the background after the initial load, before the user navigates.

Preloading strategies:

StrategyBehavior
NoPreloadingDefault — no preloading
PreloadAllModulesLoad every lazy chunk after initial
CustomLoad only selected chunks

Enable preloading:

import { provideRouter, withPreloading, PreloadAllModules } from '@angular/router';

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes, withPreloading(PreloadAllModules))
  ]
});

After the initial bundle loads, the router starts downloading all lazy chunks in the background.

What this gives: The first navigation to a lazy route is instant — the chunk is already cached. The cost is bandwidth — every lazy chunk is downloaded even if the user never visits those routes.

When preloading helps: Apps where users will likely visit most routes — dashboards, admin panels, internal tools.

When it doesn’t: Apps where most routes are rarely visited — public sites, docs, large catalogs. Preloading wastes bandwidth.

Custom preloading:

import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of } from 'rxjs';

export class SelectivePreloading implements PreloadingStrategy {
  preload(route: Route, load: () => Observable<unknown>): Observable<unknown> {
    return route.data?.['preload'] ? load() : of(null);
  }
}

The strategy checks route.data.preload and only loads marked routes.

{
  path: 'dashboard',
  loadChildren: () => import('./dashboard/dashboard.routes').then(m => m.dashboardRoutes),
  data: { preload: true }
}

Quicklink strategy: Third-party strategies exist that preload routes visible on screen. Rare, but useful for link-heavy apps.

Why preloading exists: Lazy loading trades initial speed for delayed navigation. Preloading gets the speed back — after the initial load, the chunks are fetched in the background, so by the time the user navigates, they’re cached. The best of both when the app supports it.

Why not always preload: Bandwidth and CPU cost. Downloading megabytes the user never uses wastes both. Preloading is a bet that the user will visit the routes. Sometimes the bet pays off; sometimes it doesn’t.

Why NoPreloading is the default: The safest choice. The user’s bandwidth is used only for what they visit. Apps that know their users well can opt into preloading.


Chunk naming

By default, the bundler generates hashed names. You can name chunks.

Named chunks with a comment:

loadChildren: () =>
  import(/* webpackChunkName: "admin" */ './admin/admin.routes')
    .then(m => m.adminRoutes)

The webpackChunkName comment tells webpack (and other bundlers that support it) to name the chunk admin.

Esbuild and Vite: Use a similar comment.

import(/* @vite-ignore */ './admin/admin.routes')

Or rely on the file name — modern bundlers often name chunks after the imported file.

Why naming matters: Hashed names like chunk-ABC123.js are hard to identify. Named chunks like admin.js make build output readable. Useful for debugging and for long-term cache management.

Not required: Naming is optional. The bundler works either way. Use it when the build output is hard to read.

Why the hashes: Hashed names include a content hash, so the browser caches by content. Change the code, the hash changes, the browser re-downloads. That’s cache-busting — an important production feature.

Naming + hashing: You can have both: admin.ABC123.js. The name identifies the chunk; the hash busts the cache. Many bundlers combine them.

Why webpackChunkName isn’t universal: It’s a webpack-specific magic comment. Other bundlers have their own conventions. Modern bundlers often infer names from the file path, so explicit naming is less necessary than it used to be.


A full example

An app with eager and lazy routes.

// ============================================
// EAGER COMPONENTS
// ============================================

import { Component } from '@angular/core';
import { RouterOutlet, RouterLink, RouterLinkActive } from '@angular/router';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet, RouterLink, RouterLinkActive],
  template: `
    <header>
      <nav>
        <a routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">Home</a>
        <a routerLink="/about" routerLinkActive="active">About</a>
        <a routerLink="/admin" routerLinkActive="active">Admin</a>
        <a routerLink="/shop" routerLinkActive="active">Shop</a>
      </nav>
    </header>
    <main>
      <router-outlet></router-outlet>
    </main>
  `
})
export class AppComponent {}

@Component({
  selector: 'app-home',
  standalone: true,
  template: `<h1>Home</h1>`
})
export class HomeComponent {}

@Component({
  selector: 'app-about',
  standalone: true,
  template: `<h1>About</h1>`
})
export class AboutComponent {}

// ============================================
// ROUTES
// ============================================

import { Routes } from '@angular/router';
import { HomeComponent } from './home.component';
import { AboutComponent } from './about.component';

export const routes: Routes = [
  // Eager routes
  { path: '', component: HomeComponent, pathMatch: 'full' },
  { path: 'about', component: AboutComponent },

  // Lazy group — admin
  {
    path: 'admin',
    loadChildren: () =>
      import(/* webpackChunkName: "admin" */ './admin/admin.routes')
        .then(m => m.adminRoutes)
  },

  // Lazy group — shop
  {
    path: 'shop',
    loadChildren: () =>
      import(/* webpackChunkName: "shop" */ './shop/shop.routes')
        .then(m => m.shopRoutes)
  }
];

// ============================================
// ADMIN ROUTES (lazy)
// ============================================

import { Routes } from '@angular/router';

export const adminRoutes: Routes = [
  {
    path: '',
    loadComponent: () => import('./admin-home.component').then(m => m.AdminHomeComponent)
  },
  {
    path: 'users',
    loadComponent: () => import('./users.component').then(m => m.UsersComponent)
  },
  {
    path: 'settings',
    loadComponent: () => import('./settings.component').then(m => m.SettingsComponent)
  }
];

// ============================================
// SHOP ROUTES (lazy)
// ============================================

import { Routes } from '@angular/router';

export const shopRoutes: Routes = [
  {
    path: '',
    loadComponent: () => import('./shop-home.component').then(m => m.ShopHomeComponent)
  },
  {
    path: 'products',
    loadComponent: () => import('./products.component').then(m => m.ProductsComponent)
  },
  {
    path: 'cart',
    loadComponent: () => import('./cart.component').then(m => m.CartComponent)
  }
];

// ============================================
// BOOTSTRAP
// ============================================

import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter, withPreloading, PreloadAllModules } from '@angular/router';

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes, withPreloading(PreloadAllModules))
  ]
});

What this shows:

  • Eager routes — home, about — in the main bundle
  • Lazy groups — admin, shop — each in its own chunk
  • Each group uses loadComponent for its children — further lazy
  • Named chunks with webpackChunkName
  • PreloadAllModules — after initial load, admin and shop are preloaded

Bundling result:

Initial: main.js (~200 KB) + polyfills + styles
Lazy:    admin.js, shop.js, and one chunk per child

Visiting / downloads only the initial bundle. Navigating to /admin downloads admin.js. The user never downloads /shop unless they visit it (or preloading fetches it in the background).

Why this shape: It’s the standard lazy loading setup. The shell is eager. Each section is lazy. Preloading warms the chunks for likely-navigated routes. That’s the pattern most production Angular apps use.


Complete Example Session

# ============================================
# PART 1: EAGER ROUTES
# ============================================

cat > app.routes.ts << 'EOF'
import { Routes } from '@angular/router';
import { HomeComponent } from './home.component';

export const routes: Routes = [
  { path: '', component: HomeComponent, pathMatch: 'full' }
];
EOF

# ============================================
# PART 2: LAZY COMPONENT
# ============================================

cat > lazy.routes.ts << 'EOF'
import { Routes } from '@angular/router';

export const routes: Routes = [
  { path: '', component: HomeComponent },
  {
    path: 'settings',
    loadComponent: () =>
      import('./settings.component').then(m => m.SettingsComponent)
  }
];

import { HomeComponent } from './home.component';
EOF

npx tsc --noEmit lazy.routes.ts
# (no errors)

# ============================================
# PART 3: LAZY CHILDREN
# ============================================

cat > with-children.ts << 'EOF'
import { Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: 'admin',
    loadChildren: () =>
      import('./admin.routes').then(m => m.adminRoutes)
  }
];
EOF

cat > admin.routes.ts << 'EOF'
import { Routes } from '@angular/router';

export const adminRoutes: Routes = [
  { path: '', redirectTo: 'home', pathMatch: 'full' },
  {
    path: 'home',
    loadComponent: () => import('./admin-home.component').then(m => m.AdminHomeComponent)
  },
  {
    path: 'users',
    loadComponent: () => import('./users.component').then(m => m.UsersComponent)
  }
];
EOF

npx tsc --noEmit with-children.ts admin.routes.ts
# (no errors)

# ============================================
# PART 4: NAMED CHUNKS
# ============================================

cat > named.ts << 'EOF'
import { Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: 'admin',
    loadChildren: () =>
      import(/* webpackChunkName: "admin" */ './admin.routes')
        .then(m => m.adminRoutes)
  }
];
EOF

npx tsc --noEmit named.ts
# (no errors)

# ============================================
# PART 5: PRELOADING
# ============================================

cat > preload.ts << 'EOF'
import { provideRouter, withPreloading, PreloadAllModules } from '@angular/router';
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { routes } from './app.routes';

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes, withPreloading(PreloadAllModules))
  ]
});
EOF

npx tsc --noEmit preload.ts
# (no errors)

# ============================================
# PART 6: GUARDED LAZY ROUTE
# ============================================

cat > guarded.ts << 'EOF'
import { Routes } from '@angular/router';
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';

export const authGuard: CanActivateFn = () => {
  const router = inject(Router);
  const isLoggedIn = true;
  if (isLoggedIn) return true;
  return router.createUrlTree(['/login']);
};

export const routes: Routes = [
  {
    path: 'admin',
    loadChildren: () => import('./admin.routes').then(m => m.adminRoutes),
    canActivate: [authGuard]
  }
];
EOF

npx tsc --noEmit guarded.ts
# (no errors)

# ============================================
# PART 7: CUSTOM PRELOADING
# ============================================

cat > custom-preload.ts << 'EOF'
import { Injectable } from '@angular/core';
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class SelectivePreloading implements PreloadingStrategy {
  preload(route: Route, load: () => Observable<unknown>): Observable<unknown> {
    return route.data?.['preload'] ? load() : of(null);
  }
}
EOF

npx tsc --noEmit custom-preload.ts
# (no errors)

# ============================================
# PART 8: BUILD AND CHECK
# ============================================

# Run the build to see chunk output
# ng build
# The output shows:
#   main.js
#   polyfills.js
#   styles.css
#   admin.js       (lazy)
#   shop.js        (lazy)
#   ...

cat << 'EOF'
Lazy loading checklist:

  [ ] Initial routes use `component` (eager)
  [ ] Feature routes use `loadComponent` or `loadChildren`
  [ ] No top-level imports of lazy code
  [ ] Chunks have names (webpackChunkName)
  [ ] Preloading strategy chosen
  [ ] Build output verified: lazy chunks separate
  [ ] Lazy routes tested in dev mode
  [ ] No accidental eager imports
EOF

Quick Reference

Lazy Loading Forms

FormLoads
loadComponent: () => import(...).then(m => m.C)Single component
loadChildren: () => import(...).then(m => m.routes)Route group

Syntax

// Component
loadComponent: () => import('./c.component').then(m => m.CComponent)

// Routes
loadChildren: () => import('./routes').then(m => m.routes)

Old vs New

OldNew
loadChildren: './m.module#M'loadChildren: () => import('./m.module').then(m => m.M)
loadChildren: () => import('./m.module').then(m => m.M)Same

What Gets Chunked

Import typeResult
Static importEager chunk
Dynamic import()Lazy chunk
loadComponentLazy chunk
loadChildrenLazy chunk

Bundling Output

ChunkLoads
main.jsOn first load
polyfills.jsOn first load
styles.cssOn first load
feature.jsWhen route activates

Preloading Strategies

StrategyBehavior
NoPreloading (default)No preloading
PreloadAllModulesLoad all after initial
CustomSelected routes

Enable Preloading

provideRouter(routes, withPreloading(PreloadAllModules))

Named Chunks

BundlerSyntax
Webpackimport(/* webpackChunkName: "name" */ '...')
Vite/esbuildFile name inference

Common Patterns

PatternCode
Lazy componentloadComponent: () => import(...).then(m => m.C)
Lazy grouploadChildren: () => import(...).then(m => m.routes)
Lazy with guardloadChildren: ..., canActivate: [guard]
Lazy with resolverloadChildren: ..., resolve: {...}
Lazy with dataloadChildren: ..., data: {...}
Lazy with paramspath: 'users/:id', loadComponent: ...

Guard Precedence

StepRuns
1Eager guards on the parent
2canActivate on the lazy route
3Load the lazy chunk
4Guards inside the lazy group
5Resolvers

Guards run before the chunk loads — denied routes never download.

Preloading Custom Strategy

class MyStrategy implements PreloadingStrategy {
  preload(route: Route, load: () => Observable<unknown>) {
    return route.data?.['preload'] ? load() : of(null);
  }
}

Marking a Route for Preload

{
  path: 'dashboard',
  loadChildren: () => import(...).then(m => m.routes),
  data: { preload: true }
}

Build Verification

CheckCommand
Chunk sizesng build
Bundle analysisng build --stats-json
Named chunksLook for names in output

Eager Import Trap

CodeProblem
import { C } from './lazy' in eager filePulls C into eager bundle
loadChildren: () => import('./lazy')Correct
Static import of lazy codeUndoes lazy loading

Common Lazy Loading Shapes

App sectionRoute
Admin{ path: 'admin', loadChildren: ... }
Shop{ path: 'shop', loadChildren: ... }
Settings{ path: 'settings', loadComponent: ... }
Profile{ path: 'profile', loadComponent: ... }

Error Cases

ErrorCause
ChunkLoadErrorNetwork failure or stale deploy
Cannot find moduleWrong path in import
Lazy chunk not createdStatic import used instead
Guard prevents chunkCorrect behavior
Slow first navigationNo preloading

Dev vs Prod

ModeChunk names
DevReadable, unminified
ProdHashed, minified

When to Lazy Load

SituationLazy?
Admin panel
Shop
Docs
Settings
Home page❌ (eager)
About page⚠️ (often eager)
Tiny app

Best Practices

Do This:

// Use loadComponent for single components
{
  path: 'settings',
  loadComponent: () => import('./settings.component').then(m => m.SettingsComponent)
}                                                              // ✅

// Use loadChildren for route groups
{
  path: 'admin',
  loadChildren: () => import('./admin.routes').then(m => m.adminRoutes)
}                                                              // ✅

// Name chunks for readable output
import(/* webpackChunkName: "admin" */ './admin.routes')       // ✅

// Keep the shell eager
{ path: '', component: HomeComponent }                         // ✅

// Use guards on lazy routes to avoid loading protected code
{
  path: 'admin',
  loadChildren: () => import('./admin.routes').then(m => m.adminRoutes),
  canActivate: [authGuard]
}                                                              // ✅

// Preload likely-navigated routes
provideRouter(routes, withPreloading(PreloadAllModules))       // ✅

// Use custom preloading for selective loading
class SelectivePreloading implements PreloadingStrategy { }    // ✅

// Verify chunk sizes after build
// ng build                                                   // ✅

// Lazy-load by feature, not by component
{ path: 'shop', loadChildren: ... }                            // ✅

// Keep lazy groups self-contained
// Don't share code with eager unnecessarily                    // ✅

Don’t Do This:

// Don't statically import lazy code
import { AdminComponent } from './admin.component';  // ⚠️  eager // ⚠️
{ path: 'admin', loadComponent: () => import('./admin.component').then(...) }

// Don't use the old string syntax in new code
loadChildren: './admin/admin.module#AdminModule'  // ⚠️  deprecated // ⚠️

// Don't lazy-load every route
// Small routes aren't worth a separate chunk                    // ⚠️

// Don't preload everything blindly
PreloadAllModules  // ⚠️  bandwidth cost on public sites       // ⚠️

// Don't share a lot of code between lazy chunks
// Duplicates or large common chunks                              // ⚠️

// Don't nest lazy loading deeply without reason
// { loadChildren → loadChildren → loadChildren }                 // ⚠️

// Don't forget the guard if the section is protected
// Protected code shouldn't load                                     // ⚠️

// Don't assume lazy chunks are cached forever
// Browser cache eviction                                           // ⚠️

// Don't use `import()` without the bundler recognizing it
// Bundlers need static paths                                       // ⚠️

Common Pitfalls

PitfallProblemSolution
Static import of lazy codePulls into eager bundleRemove top-level import
String loadChildrenDeprecatedUse function form
Lazy loading everythingOverhead for small routesKeep small routes eager
No preloadingSlow first lazy navigationAdd strategy
Preloading everythingBandwidth costUse selective
Chunk too largeSlow first lazy loadSplit into smaller groups
ChunkLoadErrorNetwork or stale deployReload, use hashes
Guard after chunk loadWasted downloadGuard before load
Nested lazy loading too deepComplex, slowFlatten
Lazy code shares eager codeDuplicates in bundleExtract shared chunk

Real-World Examples

1. Lazy component

{
  path: 'settings',
  loadComponent: () => import('./settings.component').then(m => m.SettingsComponent)
}

2. Lazy route group

{
  path: 'admin',
  loadChildren: () => import('./admin.routes').then(m => m.adminRoutes)
}

3. Named chunk

import(/* webpackChunkName: "admin" */ './admin.routes')

4. With guard

{
  path: 'admin',
  loadChildren: () => import('./admin.routes').then(m => m.adminRoutes),
  canActivate: [authGuard]
}

5. With resolver

{
  path: 'dashboard',
  loadChildren: () => import('./dashboard.routes').then(m => m.routes),
  resolve: { data: dashboardResolver }
}

6. With data

{
  path: 'admin',
  loadChildren: () => import('./admin.routes').then(m => m.adminRoutes),
  data: { section: 'admin' }
}

7. With params

{
  path: 'users/:id',
  loadComponent: () => import('./user-detail.component').then(m => m.UserDetailComponent)
}

8. Preloading all

provideRouter(routes, withPreloading(PreloadAllModules))

9. Selective preloading

{
  path: 'dashboard',
  loadChildren: () => import('./dashboard.routes').then(m => m.dashboardRoutes),
  data: { preload: true }
}

10. Custom strategy

class SelectivePreloading implements PreloadingStrategy {
  preload(route: Route, load: () => Observable<unknown>): Observable<unknown> {
    return route.data?.['preload'] ? load() : of(null);
  }
}

11. Register strategy

provideRouter(routes, withPreloading(SelectivePreloading))

12. Lazy child routes

// admin.routes.ts
export const adminRoutes: Routes = [
  { path: '', loadComponent: () => import('./home.component').then(m => m.HomeComponent) },
  { path: 'users', loadComponent: () => import('./users.component').then(m => m.UsersComponent) }
];

13. Redirect in lazy group

export const adminRoutes: Routes = [
  { path: '', redirectTo: 'dashboard', pathMatch: 'full' },
  { path: 'dashboard', loadComponent: () => import('./dashboard.component').then(m => m.DashboardComponent) }
];

14. Shared lazy chunk

// Both admin and shop lazy routes share a common module
// Bundler extracts into a shared chunk

15. Verify with build

ng build
# Check chunk output

16. Stats file

ng build --stats-json
# Analyze with webpack-bundle-analyzer

17. Handle ChunkLoadError

// Reload or show message on failed chunk load
// Usually a stale deploy — user has old index.html

18. Lazy with no parent component

{
  path: 'admin',
  loadChildren: () => import('./admin.routes').then(m => m.adminRoutes)
}
// No component — routes render in parent's outlet

19. Multiple lazy groups

export const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'admin', loadChildren: () => import('./admin.routes').then(m => m.adminRoutes) },
  { path: 'shop', loadChildren: () => import('./shop.routes').then(m => m.shopRoutes) },
  { path: 'docs', loadChildren: () => import('./docs.routes').then(m => m.docsRoutes) }
];

20. Nested lazy loading

{
  path: 'admin',
  loadChildren: () => import('./admin.routes').then(m => m.adminRoutes)
}

// admin.routes.ts
export const adminRoutes: Routes = [
  {
    path: 'reports',
    loadChildren: () => import('./reports.routes').then(m => m.reportsRoutes)
  }
];

Visual: Eager vs Lazy

┌──────────────────────────────────────────────┐
│  EAGER (default)                             │
│                                              │
│  All imports at top:                         │
│  import { A } from './a';                    │
│  import { B } from './b';                    │
│  import { C } from './c';                    │
│                                              │
│  → All bundled into main.js                  │
│  → Downloaded on first load                  │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  LAZY                                        │
│                                              │
│  Only shell imported:                        │
│  loadChildren: () => import('./a')           │
│  loadComponent: () => import('./b')          │
│                                              │
│  → Each import becomes a chunk               │
│  → Downloaded on route activation            │
│                                              │
└──────────────────────────────────────────────┘

Visual: Chunk Loading

┌──────────────────────────────────────────────┐
│  First load: /                               │
│                                              │
│  Browser downloads:                          │
│  ├── main.js                                 │
│  ├── polyfills.js                            │
│  └── styles.css                              │
│                                              │
│  Home component renders                      │
│                                              │
└──────────────────────────────────────────────┘
                  │
                  │  navigate to /admin
                  ▼
┌──────────────────────────────────────────────┐
│  Router activates /admin                     │
│       │                                      │
│       ▼                                      │
│  Calls loadChildren()                        │
│       │                                      │
│       ▼                                      │
│  Browser downloads admin chunk               │
│       │                                      │
│       ▼                                      │
│  Admin routes registered                     │
│       │                                      │
│       ▼                                      │
│  Admin component renders                     │
│                                              │
└──────────────────────────────────────────────┘

Visual: loadComponent vs loadChildren

┌──────────────────────────────────────────────┐
│  loadComponent                               │
│                                              │
│  {                                           │
│    path: 'settings',                         │
│    loadComponent: () => import('./s').then(m => m.S)│
│  }                                           │
│                                              │
│  → One component in the chunk                │
│  → No child routes                           │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  loadChildren                                │
│                                              │
│  {                                           │
│    path: 'admin',                            │
│    loadChildren: () => import('./r').then(m => m.routes)│
│  }                                           │
│                                              │
│  → A Routes array in the chunk               │
│  → Multiple child routes                     │
│                                              │
└──────────────────────────────────────────────┘

Visual: Bundling

┌──────────────────────────────────────────────┐
│  Source:                                     │
│                                              │
│  app.routes.ts                               │
│  ├── import Home (static)                    │
│  ├── loadComponent Settings                  │
│  └── loadChildren Admin                      │
│                                              │
└──────────────────────────────────────────────┘
                  │
                  │  bundler
                  ▼
┌──────────────────────────────────────────────┐
│  Chunks:                                     │
│                                              │
│  main.js                                     │
│  ├── App                                     │
│  ├── Home                                    │
│  └── router                                  │
│                                              │
│  settings.js    ← lazy                       │
│  admin.js       ← lazy                       │
│                                              │
└──────────────────────────────────────────────┘

Visual: Eager Import Trap

┌──────────────────────────────────────────────┐
│  ❌ Wrong                                    │
│                                              │
│  // app.routes.ts                            │
│  import { AdminComponent } from './admin';   │
│                                              │
│  {                                           │
│    path: 'admin',                            │
│    loadComponent: () => import('./admin').then(m => m.AdminComponent)│
│  }                                           │
│                                              │
│  Static import pulls AdminComponent          │
│  into main.js — not lazy                     │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  ✅ Right                                    │
│                                              │
│  // app.routes.ts                            │
│  // no import of AdminComponent              │
│                                              │
│  {                                           │
│    path: 'admin',                            │
│    loadComponent: () => import('./admin').then(m => m.AdminComponent)│
│  }                                           │
│                                              │
│  Only the dynamic import references it       │
│  Bundler creates a separate chunk            │
│                                              │
└──────────────────────────────────────────────┘

Visual: Preloading

┌──────────────────────────────────────────────┐
│  Without preloading:                         │
│                                              │
│  Load main.js                                │
│  Wait                                         │
│  User clicks Admin                           │
│  Download admin.js ← delay here              │
│  Admin renders                               │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  With preloading:                            │
│                                              │
│  Load main.js                                │
│  Background: download admin.js, shop.js      │
│  Wait (chunks cached)                        │
│  User clicks Admin                           │
│  Instant — chunk already cached              │
│  Admin renders                               │
│                                              │
└──────────────────────────────────────────────┘

Visual: Chunk Loading Decision

┌──────────────────────────────────────────────┐
│  Route activated                             │
│       │                                      │
│       ▼                                      │
│  Guards run                                  │
│       │                                      │
│       ├── Denied → chunk not downloaded      │
│       │                                      │
│       └── Allowed                            │
│              │                               │
│              ▼                               │
│         Chunk already in cache?              │
│              │                               │
│              ├── Yes → instant               │
│              │                               │
│              └── No → download chunk         │
│                    │                         │
│                    ▼                         │
│              Render component                │
│                                              │
└──────────────────────────────────────────────┘

Visual: Shared Chunks

┌──────────────────────────────────────────────┐
│  admin.routes imports lodash                 │
│  shop.routes imports lodash                  │
│                                              │
└──────────────────────────────────────────────┘
                  │
                  │  bundler analyzes
                  ▼
┌──────────────────────────────────────────────┐
│  lodash extracted into common chunk          │
│                                              │
│  chunks:                                     │
│  ├── common.js     (lodash)                  │
│  ├── admin.js      (admin code)              │
│  └── shop.js       (shop code)               │
│                                              │
│  Both admin and shop load common.js          │
│                                              │
└──────────────────────────────────────────────┘

Visual: When to Lazy Load

┌──────────────────────────────────────────────┐
│  Will most users visit this route?           │
│       │                                      │
│       ├── Yes ──► Eager (or preload)         │
│       │                                      │
│       └── No                                  │
│            │                                 │
│            ├── Feature group? ──► loadChildren│
│            │                                 │
│            └── Single component? ──► loadComponent│
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  Protected by auth?                          │
│       │                                      │
│       └── Yes ──► Lazy + guard               │
│                   (denied users don't load)  │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  Large code?                                 │
│       │                                      │
│       └── Yes ──► Lazy                       │
│                                              │
└──────────────────────────────────────────────┘

Visual: Build Output

┌──────────────────────────────────────────────┐
│  $ ng build                                  │
│                                              │
│  Initial chunk files    | Names     | Raw size│
│  main.js                | main      | 250 kB │
│  polyfills.js           | polyfills | 100 kB │
│  styles.css             | styles    | 5 kB   │
│                                              │
│  Lazy chunk files       | Names     | Raw size│
│  chunk-ABC.js           | admin     | 80 kB  │
│  chunk-DEF.js           | shop      | 120 kB │
│  chunk-GHI.js           | settings  | 20 kB  │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  Initial download:                           │
│  250 + 100 + 5 = 355 kB                      │
│                                              │
│  Full app if all visited:                    │
│  355 + 80 + 120 + 20 = 575 kB                │
│                                              │
│  Savings: 220 kB on initial load             │
│                                              │
└──────────────────────────────────────────────┘

Visual: Decision Flow

┌──────────────────────────────────────────────┐
│  Should this route be lazy?                  │
│       │                                      │
│       ├── Home page? ──► Eager               │
│       ├── Frequently visited? ──► Eager      │
│       ├── Feature group? ──► loadChildren    │
│       ├── Single component? ──► loadComponent│
│       └── Protected? ──► Lazy + guard        │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  Enable preloading?                          │
│       │                                      │
│       ├── Internal app ──► Yes               │
│       ├── Public site ──► No                 │
│       └── Selective ──► Custom strategy      │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  Verify the bundle?                          │
│       │                                      │
│       ├── Check chunk sizes ──► ng build     │
│       └── No eager imports ──► grep          │
│                                              │
└──────────────────────────────────────────────┘

Summary

ConceptMeaning
Lazy loadingDefer downloading code until needed
loadComponentLazy-load a single component
loadChildrenLazy-load a route group
import()Dynamic import — creates a chunk
ChunkSeparate bundle for a lazy route
PreloadingLoad chunks after initial in background
PreloadAllModulesPreload every lazy chunk
EagerBundled in main.js — loads first
Chunk namingwebpackChunkName comment

Key takeaways:

  • Lazy loading splits code into chunks that load on demand
  • loadComponent lazy-loads a single component
  • loadChildren lazy-loads a route group
  • Both use dynamic import() — the bundler creates a chunk
  • The initial bundle contains only eager routes — everything else is deferred
  • Remove static imports of lazy code — otherwise it’s eager
  • Guards run before the chunk loads — denied routes don’t download
  • Preloading downloads chunks in the background after initial load
  • PreloadAllModules loads every chunk; custom strategies load selectively
  • Name chunks with webpackChunkName for readable build output
  • Verify the bundle with ng build — check that lazy chunks are separate
  • Don’t lazy-load small or frequently visited routes — the overhead isn’t worth it
  • Lazy-load by feature, not by component

Remember: Lazy loading is about shipping less on the initial load. The shell stays eager; features load on demand. loadComponent for single components, loadChildren for groups. The bundler splits at every import() — so remove static imports of lazy code, or the split doesn’t happen. Add preloading when users will visit most routes. Verify the build output. Done right, the first paint is fast and the rest of the app loads as the user needs it.


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!