Lazy Loading — loadComponent and loadChildren

📋 Table of Contents
  1. Lazy Loading Routes
  2. Common Mistakes

Lazy loading defers loading JavaScript chunks until they are needed, reducing the initial bundle size and improving first-load performance. Without lazy loading, the entire application is bundled into one JavaScript file that the browser must download before displaying anything. With lazy loading, only the code for the initial route is loaded immediately; feature code is downloaded on demand when the user navigates to that feature. Angular’s router uses dynamic import() to split the bundle at route boundaries.

Lazy Loading Routes

// ── app.routes.ts — lazy loading structure for BlogApp ────────────────────
export const routes: Routes = [
  // ── Eagerly loaded (part of main bundle) ──────────────────────────────
  { path: '', redirectTo: 'posts', pathMatch: 'full' },

  // ── Lazy-loaded standalone component ────────────────────────────────────
  // Webpack creates a separate JS chunk for PostListComponent and its deps
  {
    path: 'posts',
    loadComponent: () =>
      import('./features/posts/post-list.component').then(m => m.PostListComponent),
    title: 'Posts',
  },
  {
    path: 'posts/:slug',
    loadComponent: () =>
      import('./features/posts/post-detail.component').then(m => m.PostDetailComponent),
  },

  // ── Lazy-loaded route subtree (entire feature) ────────────────────────
  // auth.routes.ts contains all auth-related routes in one lazy chunk
  {
    path: 'auth',
    loadChildren: () =>
      import('./features/auth/auth.routes').then(m => m.authRoutes),
  },
  {
    path: 'admin',
    canActivate: [authGuard, adminGuard],
    loadChildren: () =>
      import('./features/admin/admin.routes').then(m => m.adminRoutes),
  },

  { path: '**', loadComponent: () => import('./shared/not-found.component')
    .then(m => m.NotFoundComponent) },
];

// ── features/auth/auth.routes.ts — the lazy chunk's routes ────────────────
export const authRoutes: Routes = [
  {
    path: '',
    loadComponent: () => import('./auth-layout.component').then(m => m.AuthLayoutComponent),
    children: [
      { path: 'login',    loadComponent: () => import('./login.component').then(m => m.LoginComponent) },
      { path: 'register', loadComponent: () => import('./register.component').then(m => m.RegisterComponent) },
      { path: '',         redirectTo: 'login', pathMatch: 'full' },
    ],
  },
];

// ── Preloading strategies ──────────────────────────────────────────────────
// PreloadAllModules: after initial load, silently preload all lazy routes
// NoPreloading: only load when user navigates to that route (default)
// Custom: define a strategy that preloads selected routes

// Custom preload strategy — preload authenticated routes when user is logged in
@Injectable({ providedIn: 'root' })
export class AuthenticatedPreloadStrategy implements PreloadingStrategy {
  private auth = inject(AuthService);

  preload(route: Route, load: () => Observable<any>): Observable<any> {
    // Preload admin routes only when the user is an admin
    if (route.path === 'admin' && this.auth.hasRole('Admin')) {
      return load();
    }
    return EMPTY;  // don't preload other routes
  }
}

// provideRouter(routes, withPreloading(AuthenticatedPreloadStrategy))
Note: loadComponent is for lazy-loading a single standalone component as a route. loadChildren is for lazy-loading an entire route subtree (multiple routes in a feature). Both use dynamic import() which Webpack (or esbuild in Angular 18) recognises as a code split point — the imported module is compiled into a separate JavaScript chunk file. The chunk is downloaded from the server the first time the user navigates to that route, then cached by the browser for subsequent visits.
Tip: Measure your bundle sizes with ng build --stats-json then npx webpack-bundle-analyzer dist/blog-app/stats.json. This shows a visual treemap of bundle composition. Aim for a main bundle under 150KB gzipped. Move any large libraries (markdown editors, chart libraries, rich text editors) into lazy-loaded route chunks so they are only downloaded when the user navigates to the feature that needs them. Libraries like code editors (Monaco) and PDF generators should always be lazy-loaded.
Warning: PreloadAllModules preloads all lazy routes silently in the background after the initial load completes. This reduces navigation latency for subsequent routes but increases the total data downloaded on first visit. On slow connections, this can consume bandwidth the user did not intentionally request. Prefer a custom preloading strategy that only preloads routes the user is likely to visit (e.g., the logged-in user’s most-visited features) or use NoPreloading for applications with many large lazy routes.

Common Mistakes

Mistake 1 — Eager-loading large features (slow initial load)

❌ Wrong — importing AdminDashboardComponent in app.routes.ts with component: instead of loadComponent:; the entire admin bundle is in the main chunk.

✅ Correct — all non-initial-page components use loadComponent or loadChildren.

Mistake 2 — Not code-splitting large third-party libraries (oversized main bundle)

❌ Wrong — importing a rich text editor in a service that is loaded at startup; adds 500KB to the main bundle.

✅ Correct — import large libraries only inside lazy-loaded component files; Webpack includes them in the lazy chunk, not the main bundle.

🧠 Test Yourself

With PreloadAllModules, when does the admin route chunk download if the user never navigates to /admin?