---
title: "Maximizing Performance with Lazy Loading and Preload Routing in Angular"
date: "2022-10-22"
slug: "maximizing-performance-with-lazy-loading-and-preload-routing-in-angular"
author: "Dany Paredes"
canonical: "https://danywalls.com/maximizing-performance-with-lazy-loading-and-preload-routing-in-angular"
description: "Improve Angular application speed and initial bundle size using lazy loading routes with loadComponent, custom preloading strategies, and modern @defer template views."
---


> ⚡ **Using Modern Angular (v17+)?** Jump directly to [Modern Lazy Loading with Standalone Components & @defer](#modern-lazy-loading-with-standalone-components--defer) to see `loadComponent`, `provideRouter()`, and `@defer` views in action.

When building an enterprise Angular application with multiple features, bundling all components into a single `main.js` bundle hurts your initial load time. Users end up waiting for screens and features they might never even visit during their session.

To deliver great Core Web Vitals and snappy user experiences, we must split our application into on-demand chunks using **Lazy Loading** and **Preload Strategies**.

In this guide, I will show you how lazy loading works in both legacy `NgModule` architectures and modern **Standalone** applications, how to configure custom preloading strategies, and how to combine route splitting with template-level **`@defer`** views.

---

## The Scenario: A Multi-Feature Payment App

Imagine we are building a fintech dashboard with four distinct feature areas:
1. `Dashboard` (main landing page)
2. `MoneyTransfer`
3. `Wallet`
4. `Activity`

Let's look at what happens when all features are bundled together versus lazy-loaded.

---

## 1. Legacy Approach: NgModule Lazy Loading & Preloading

In older versions of Angular (v2 – v16), lazy loading required a dedicated `Routing.module` for each feature:

```typescript
// app-routing.module.ts (Legacy NgModule)
import { NgModule } from '@angular/core';
import { RouterModule, Routes, PreloadAllModules } from '@angular/router';

const routes: Routes = [
  { path: '', redirectTo: '/dashboard', pathMatch: 'full' },
  {
    path: 'wallet',
    loadChildren: () => import('./wallet/wallet.module').then((m) => m.WalletModule),
  },
  {
    path: 'activity',
    loadChildren: () => import('./activity/activity.module').then((m) => m.ActivityModule),
  },
  {
    path: 'dashboard',
    loadChildren: () => import('./dashboard/dashboard.module').then((m) => m.DashboardModule),
  },
  {
    path: 'money',
    loadChildren: () => import('./moneytransfer/moneytransfer.module').then((m) => m.MoneyTransferModule),
  },
];

@NgModule({
  imports: [
    RouterModule.forRoot(routes, {
      preloadingStrategy: PreloadAllModules, // Preloads all modules in the background
    }),
  ],
  exports: [RouterModule],
})
export class AppRoutingModule {}
```

While this reduced the initial `main.js` bundle size, creating intermediate routing modules for every single screen created unnecessary boilerplate.

---

## Modern Lazy Loading with Standalone Components & @defer

In modern Angular (v17+ / v19), we configure routing cleanly without `NgModule` using `provideRouter()` and `loadComponent`:

### 1. Route-Level Lazy Loading with `loadComponent`

```typescript
// app.routes.ts (Modern Standalone Routes)
import { Routes } from '@angular/router';

export const routes: Routes = [
  { path: '', redirectTo: 'dashboard', pathMatch: 'full' },
  {
    path: 'dashboard',
    loadComponent: () =>
      import('./dashboard/dashboard.component').then((m) => m.DashboardComponent),
  },
  {
    path: 'wallet',
    loadComponent: () =>
      import('./wallet/wallet.component').then((m) => m.WalletComponent),
    data: { preload: true }, // Route flag for custom preloading
  },
  {
    path: 'money',
    loadComponent: () =>
      import('./money/money.component').then((m) => m.MoneyComponent),
  },
  {
    path: 'activity',
    loadChildren: () =>
      import('./activity/activity.routes').then((m) => m.ACTIVITY_ROUTES),
  },
];
```

### 2. Application Config with `provideRouter`

In `app.config.ts`, we initialize the router with modern features:

```typescript
// app.config.ts
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter, withPreloading, PreloadAllModules } from '@angular/router';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideZoneChangeDetection({ eventCoalescing: true }),
    provideRouter(
      routes,
      withPreloading(PreloadAllModules) // Native preloading helper
    ),
  ],
};
```

---

## Building a Custom Preload Strategy

`PreloadAllModules` downloads every lazy chunk immediately after the initial page renders. In large applications, this can waste mobile bandwidth.

A **Custom Preload Strategy** lets you selectively preload only high-priority routes (for example, routes marked with `data: { preload: true }`):

```typescript
// custom-preloading.strategy.ts
import { Injectable } from '@angular/core';
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of } from 'rxjs';

@Injectable({
  providedIn: 'root',
})
export class SelectivePreloadingStrategy implements PreloadingStrategy {
  preload(route: Route, load: () => Observable<any>): Observable<any> {
    // Check if the route has the preload flag enabled
    if (route.data && route.data['preload']) {
      return load();
    }
    return of(null);
  }
}
```

### Registering the Strategy in `app.config.ts`:

```typescript
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter, withPreloading } from '@angular/router';
import { routes } from './app.routes';
import { SelectivePreloadingStrategy } from './custom-preloading.strategy';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(
      routes,
      withPreloading(SelectivePreloadingStrategy)
    ),
  ],
};
```

---

## Granular Lazy Loading with `@defer` Blocks

Route-level lazy loading splits full pages. But what about heavy widgets *inside* a page, like transaction charts or PDF export tools?

Modern Angular provides **Deferrable Views (`@defer`)** to lazy-load individual components right from your templates:

```html
<!-- dashboard.component.html -->
<div class="dashboard-container">
  <h1>Welcome to your Dashboard</h1>

  <!-- Immediate critical content -->
  <app-account-summary />

  <!-- Lazy-loaded below the fold when scrolled into view -->
  @defer (on viewport) {
    <app-heavy-spending-chart [transactions]="transactions()" />
  } @placeholder {
    <div class="chart-skeleton">Loading spending analytics...</div>
  } @loading (minimum 300ms) {
    <div class="spinner">Fetching chart assets...</div>
  } @error {
    <p>Failed to load analytics chart.</p>
  }
</div>
```

### Why `@defer` is superior for component-level code splitting:
1. **Zero router configuration required:** Angular build automatically generates a separate JS chunk for `<app-heavy-spending-chart>`.
2. **Interactive triggers:** Load on `viewport`, `interaction` (click/focus), `hover`, `timer(2s)`, or `idle`.
3. **Built-in UX states:** Native `@placeholder`, `@loading`, and `@error` blocks prevent layout shifting.

---

## Strategy Comparison Matrix

| Strategy | When to Use | Benefit |
| :--- | :--- | :--- |
| **`loadComponent`** | Separate application views and pages | Reduces initial `main.js` bundle size |
| **`PreloadAllModules`** | Small-to-medium apps with stable network | Instant page transitions after initial load |
| **`SelectivePreloadingStrategy`** | Large enterprise apps with restricted mobile bandwidth | Preloads only high-conversion or frequent routes |
| **`@defer (on viewport)`** | Heavy widgets, graphs, modals, and rich editors | Defers JavaScript download until component is actually visible |

---

## Summary

Optimizing performance in modern Angular requires a multi-layered approach:

1. Use **`loadComponent`** in your route definitions to split pages into Standalone chunks.
2. Configure **`withPreloading(SelectivePreloadingStrategy)`** in `provideRouter()` to preload high-value routes in the background.
3. Leverage **`@defer (on viewport)`** inside templates to defer heavy third-party components and widgets until the user needs them.



