Advanced Topics

Performance Optimization

Optimize your Angular application's performance — OnPush change detection, lazy loading, tree shaking, bundle analysis, and runtime optimizations.

Why Performance Matters

Fast applications provide better user experiences, higher engagement, and better SEO rankings. Angular provides many built-in tools for optimization.

Change Detection Strategies

Default vs OnPush

Angular's default change detection checks every component when any event occurs. OnPush limits checks to only when inputs change or events fire within the component.

typescript
// DEFAULT: Checked every change detection cycle (less performant)
@Component({ ... })
export class DefaultComponent {
  items: Item[] = [];
}

// ONPUSH: Only checked when @Input() changes or event occurs (more performant)
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    @for (item of items; track item.id) {
      <app-item-card [item]="item" />
    }
  `,
})
export class OptimizedComponent {
  @Input() items: Item[] = [];
}

OnPush Rules

For OnPush to work correctly:

Triggers Re-renderDoes NOT Trigger
@Input() reference changeMutating an object/array
DOM events in the componentParent change detection (unless input changed)
async pipe emissionsetTimeout / setInterval (external)
Signals changingDirect property changes from service subscriptions
Manual markForCheck()

Immutable Updates with OnPush

typescript
// ❌ WRONG — Mutation won't trigger OnPush
this.items.push(newItem);

// ✅ CORRECT — New array reference
this.items = [...this.items, newItem];

// ✅ CORRECT — New object reference
this.user = { ...this.user, name: 'Updated' };

TrackBy in Loops

When rendering lists, always use track to help Angular identify items:

html
<!-- ❌ Without track: entire list re-renders on change -->
@for (user of users; track $index) {
  <app-user-card [user]="user" />
}

<!-- ✅ With unique track: only changed items re-render -->
@for (user of users; track user.id) {
  <app-user-card [user]="user" />
}

Lazy Loading

Route-Level Lazy Loading

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

Defer Loading (Angular 17+)

Load components lazily based on triggers:

html
<!-- Load when visible in viewport -->
@defer (on viewport) {
  <app-heavy-chart [data]="chartData" />
} @placeholder {
  <div class="chart-skeleton">Loading chart...</div>
} @loading (minimum 500ms) {
  <app-spinner />
}

<!-- Load on interaction -->
@defer (on interaction) {
  <app-comments [postId]="post.id" />
} @placeholder {
  <button>Load Comments</button>
}

<!-- Load when idle -->
@defer (on idle) {
  <app-recommendations />
}

<!-- Load on hover -->
@defer (on hover) {
  <app-tooltip [text]="helpText" />
} @placeholder {
  <span>ℹ️</span>
}

Image Optimization

NgOptimizedImage

typescript
import { NgOptimizedImage } from '@angular/common';

@Component({
  imports: [NgOptimizedImage],
  template: `
    <!-- Prioritize above-the-fold images -->
    <img ngSrc="/hero.jpg" width="1200" height="600" priority>

    <!-- Lazy load below-the-fold images (default) -->
    <img ngSrc="/product.jpg" width="400" height="300" placeholder>

    <!-- Responsive images -->
    <img ngSrc="/photo.jpg"
         width="800" height="600"
         sizes="(max-width: 768px) 100vw, 50vw"
         [ngSrcset]="'200w, 400w, 800w, 1200w'">
  `,
})
export class ImageComponent {}

Virtual Scrolling

For large lists, render only visible items:

typescript
import { ScrollingModule } from '@angular/cdk/scrolling';

@Component({
  imports: [ScrollingModule],
  template: `
    <cdk-virtual-scroll-viewport itemSize="72" class="list-viewport">
      <div *cdkVirtualFor="let item of items; trackBy: trackById" class="list-item">
        {{ item.name }}
      </div>
    </cdk-virtual-scroll-viewport>
  `,
  styles: [`
    .list-viewport { height: 500px; }
    .list-item { height: 72px; }
  `],
})
export class LargeListComponent {
  items = Array.from({ length: 100000 }, (_, i) => ({ id: i, name: `Item ${i}` }));
  trackById = (_: number, item: { id: number }) => item.id;
}

Bundle Optimization

Analyze Bundle Size

bash
# Install bundle analyzer
npm install --save-dev webpack-bundle-analyzer

# Build with stats
ng build --stats-json

# Analyze
npx webpack-bundle-analyzer dist/my-app/stats.json

Tree Shaking Tips

typescript
// ✅ Import only what you need
import { MatButtonModule } from '@angular/material/button';

// ❌ Don't import entire libraries
import * as _ from 'lodash';

// ✅ Import specific functions
import { debounce } from 'lodash-es/debounce';

Preloading Strategies

typescript
// Preload all lazy modules after initial load
import { PreloadAllModules } from '@angular/router';

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

Runtime Optimizations

Memoize Expensive Computations

typescript
// ❌ Function called every change detection cycle
@Component({
  template: `<p>{{ getExpensiveValue() }}</p>`,
})

// ✅ Use computed signals or pipes for memoization
@Component({
  template: `<p>{{ expensiveValue() }}</p>`,
})
export class OptimizedComponent {
  data = signal(rawData);
  expensiveValue = computed(() => {
    return this.data().reduce((sum, item) => sum + item.calculate(), 0);
  });
}

Pure Pipes for Template Computations

typescript
// Pure pipe only recalculates when input changes
@Pipe({ name: 'filterActive', pure: true, standalone: true })
export class FilterActivePipe implements PipeTransform {
  transform(items: User[]): User[] {
    return items.filter(item => item.active);
  }
}
html
<!-- Efficient: pipe only recalculates when users reference changes -->
@for (user of users | filterActive; track user.id) {
  <app-user-card [user]="user" />
}

Detach Change Detection

For rarely-updated views:

typescript
export class StaticComponent {
  private cdr = inject(ChangeDetectorRef);

  ngOnInit() {
    this.cdr.detach(); // Stop automatic checks

    // Manually check when needed
    setInterval(() => {
      this.cdr.detectChanges();
    }, 5000); // Update every 5 seconds
  }
}

Web Workers

Offload CPU-intensive work:

bash
ng generate web-worker heavy-task
typescript
// heavy-task.worker.ts
addEventListener('message', ({ data }) => {
  const result = performHeavyComputation(data);
  postMessage(result);
});

// component
export class DataComponent {
  processData(data: number[]) {
    const worker = new Worker(new URL('./heavy-task.worker', import.meta.url));
    worker.onmessage = ({ data }) => {
      this.result = data;
    };
    worker.postMessage(data);
  }
}

Performance Checklist

TechniqueImpactEffort
OnPush change detectionHighMedium
track in loopsHighLow
Lazy loading routesHighLow
@defer blocksHighLow
Virtual scrollingHighMedium
NgOptimizedImageMediumLow
Pure pipesMediumLow
Bundle analysisVariableLow
Web WorkersHighHigh

Next Steps

Your application is optimized! The final topic covers deploying your Angular application — building for production, deployment platforms, and CI/CD pipelines.