Tutorials Logic, IN info@tutorialslogic.com

Angular Lazy Loading Route Level Code Splitting

Lazy-loading Boundary

Lazy loading splits route or template code from the initial bundle. Choose boundaries that reduce initial work without producing a maze of tiny chunks, then test loading, error, preloading, and layout-stability behavior.

Lazy loading moves feature code out of the initial bundle and loads it when a route or deferred template needs it. The benefit depends on bundle size, navigation patterns, caching, and network conditions, so confirm it in build and runtime measurements.

Use loadComponent for a standalone route and loadChildren for a lazy route collection. Keep route-level providers beside the lazy route when state should be created on entry and released with that route environment.

Each boundary adds a request and a loading state, so split by meaningful features rather than every small component. Verify build output because a shared eager import can pull a dependency back into the initial bundle.

  • Reduces initial bundle size - only the home page code loads first.
  • Faster initial page load - critical for mobile users.
  • Better Core Web Vitals scores.
  • Angular CLI automatically creates separate chunks for lazy-loaded routes.

Lazy Routes

Use loadComponent for one standalone route and loadChildren for a route collection. The dynamic import expression creates a bundler boundary only when the imported feature is not also pulled into an eager dependency graph.

Lazy Loading with loadComponent and loadChildren

Lazy Loading with loadComponent and loadChildren
import { Routes } from '@angular/router';

export const routes: Routes = [
    // Eagerly loaded - always in the main bundle
    { path: '', loadComponent: () => import('./home/home.component').then(m => m.HomeComponent) },

    // Lazy loaded single component - separate chunk
    {
        path: 'about',
        loadComponent: () => import('./about/about.component').then(m => m.AboutComponent)
    },

    // Lazy loaded feature module (group of routes) - separate chunk
    {
        path: 'admin',
        loadChildren: () => import('./admin/admin.routes').then(m => m.adminRoutes)
    },

    // Lazy loaded with route guard
    {
        path: 'dashboard',
        loadComponent: () => import('./dashboard/dashboard.component').then(m => m.DashboardComponent),
        canActivate: [authGuard]
    }
];

Lazy Route Collection

Lazy Route Collection
// admin/admin.routes.ts - loaded only when user visits /admin
import { Routes } from '@angular/router';

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

Deferred Templates

A @defer block creates a template-level loading boundary with configurable triggers, prefetch behavior, placeholder, loading, and error states. Use it for deferrable view dependencies; use lazy routes when navigation owns the boundary.

@defer Block - Template Lazy Loading

@defer Block - Template Lazy Loading
import { Component } from '@angular/core';
import { HeavyChartComponent } from './heavy-chart.component';
import { CommentSectionComponent } from './comment-section.component';

@Component({
    selector: 'app-page',
    standalone: true,
    template: `
        <h1>Article Title</h1>
        <p>Article content...</p>

        <!-- Load chart only when it enters the viewport -->
        @defer (on viewport) {
            <app-heavy-chart />
        } @loading {
            <p>Loading chart...</p>
        } @placeholder {
            <div class="chart-placeholder">Chart will appear here</div>
        } @error {
            <p>Failed to load chart.</p>
        }

        <!-- Load comments only on user interaction -->
        @defer (on interaction) {
            <app-comment-section />
        } @placeholder {
            <button>Load Comments</button>
        }

        <!-- Load after 2 seconds idle time -->
        @defer (on idle; prefetch on immediate) {
            <app-recommendations />
        }
    `
})
export class PageComponent {}

Preloading

A preloading strategy may fetch lazy route code after initial navigation so later routes open faster. PreloadAllModules favors future navigation speed; NoPreloading preserves bandwidth until a route is requested.

A custom PreloadingStrategy can inspect route data and likely intent, but it should remain predictable and measurable. Preloading downloads code; it should not silently perform feature data requests or start expensive work.

Measure initial load, total transferred bytes, and target-route latency before adopting a broad strategy, especially for mobile or metered users.

Strategy Behaviour Best for
NoPreloading (default) Load only on navigation Bandwidth-sensitive apps
PreloadAllModules Preload all lazy modules after initial load Small to medium apps
Custom strategy Preload only routes with a specific flag Large apps with selective preloading

Preloading Strategy

Preloading Strategy
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)  // preload all lazy modules in background
        )
    ]
};

Route Providers and Matching

Place feature providers on the lazy route when their state should be created for that route environment and released when it is destroyed. Use canMatch when a route definition should be skipped before its lazy code is selected, such as choosing separate admin and user implementations for the same path.

A guard controls navigation experience, not server authorization. The backend must still verify every protected request. Avoid importing the lazy component into a root guard or eager service because that can pull the code back into the initial bundle.

  • Use route data to drive a selective preloading strategy without loading protected data.
  • Keep the loading indicator at the navigation or defer boundary that owns the wait.
  • Handle a chunk-load failure with retry or a reload prompt after a deployment changes asset hashes.

Lazy Feature with Route-scoped State

Lazy Feature with Route-scoped State
export const routes: Routes = [
  {
    path: 'reports',
    canMatch: [reportsAccessGuard],
    providers: [ReportsFilterStore],
    loadChildren: () =>
      import('./reports/reports.routes').then(m => m.REPORT_ROUTES)
  }
];

Build and Runtime Verification

Verify a lazy boundary in both the production build output and the browser Network panel. The initial navigation should not download the feature chunk, the target navigation should request it once, and a later visit should use the browser cache according to the deployment policy.

Server rendering can render a lazy route on the server, so the route still needs browser hydration and asset testing. Keep browser-only APIs behind platform-safe boundaries and verify direct navigation to every lazy URL through the production web server fallback.

  • Compare initial JavaScript, total transferred bytes, and target-route latency before and after splitting.
  • Test direct refresh, offline or slow-network behavior, and a stale client after deployment.
  • Use bundle analysis to detect an eager import that defeats the intended chunk boundary.

Inspect Production Chunks

Inspect Production Chunks
ng build

# Open the built application through its production-style server.
# Confirm the reports chunk is absent initially and requested on /reports.
Confirm the page outcome

Loading Review

5 checks
  • I can choose among loadComponent, loadChildren, @defer, and eager loading based on ownership.
  • I can keep route providers and canMatch checks beside the feature without importing its implementation eagerly.
  • I provide loading, error, and chunk-recovery states at the boundary that owns the wait.
  • I choose preloading from navigation evidence and bandwidth constraints, not as a blanket default.
  • I verify chunks, direct refresh, SSR or hydration behavior, caching, and deployment rollover in production output.

Loading Review Questions

A static import elsewhere can pull the component into the eager dependency graph even when a route also references it through loadComponent. Search barrel files, root component imports, shared configuration, and direct references in eager templates.

Lazy loading still reduces what blocks the initial route, but PreloadAllModules begins fetching every lazy route after startup.

Reserve the space in the placeholder.

Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.