Skip to main content

Performance Optimization

Angular includes many optimizations out of the box, but as applications grow, you may need to fine-tune both how quickly your app loads and how responsive it feels during use. This guide covers the tools and techniques Angular provides to help you build fast applications.

Loading Performance

Loading performance determines how quickly your application becomes visible and interactive. Slow loading directly impacts Core Web Vitals like Largest Contentful Paint (LCP) and Time to First Byte (TTFB).

Lazy Loading Routes

Lazy loading defers loading route components until navigation, reducing the initial bundle size:
import { Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: '',
    loadComponent: () => import('./home/home.component').then(m => m.HomeComponent)
  },
  {
    path: 'dashboard',
    loadComponent: () => import('./dashboard/dashboard.component').then(m => m.DashboardComponent)
  },
  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.routes').then(m => m.ADMIN_ROUTES)
  }
];
When to use: Applications with multiple routes where not all are needed on initial load. This is one of the most effective ways to reduce initial bundle size.

Deferred Loading with @defer

The @defer block splits components into separate bundles that load on demand:
@defer {
  <large-component />
} @placeholder {
  <p>Loading content...</p>
}
@defer (on viewport) {
  <reviews-list />
}
Loads when the placeholder scrolls into view
When to use: Components not visible on initial render, heavy third-party libraries, below-the-fold content.

Prefetching Deferred Content

You can prefetch deferred content before it’s needed:
@defer (on viewport; prefetch on idle) {
  <product-recommendations />
} @placeholder {
  <div class="recommendations-skeleton"></div>
}

Immediate Prefetch

@defer (prefetch on immediate)
Starts prefetching immediately

Idle Prefetch

@defer (prefetch on idle)
Prefetches when browser is idle

Hover Prefetch

@defer (prefetch on hover)
Prefetches on hover over placeholder

Timer Prefetch

@defer (prefetch on timer(2s))
Prefetches after specified time

Runtime Performance

Runtime performance determines how responsive your application feels after it loads. Angular’s change detection system keeps the DOM in sync with your data, and optimizing how and when it runs is the primary lever for improving runtime performance.

OnPush Change Detection

OnPush change detection instructs Angular to run change detection for a component subtree only when:
  • The root component of the subtree receives new inputs (compared with ==)
  • Angular handles an event in the subtree’s root component or any of its children
import { Component, ChangeDetectionStrategy, input, output } from '@angular/core';
import { User } from './user.model';

@Component({
  selector: 'app-user-profile',
  templateUrl: './user-profile.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush  // Enable OnPush
})
export class UserProfileComponent {
  readonly user = input.required<User>();
  readonly userUpdated = output<User>();
  
  updateUser(updates: Partial<User>) {
    // This will trigger change detection for this component
    this.userUpdated.emit({ ...this.user(), ...updates });
  }
}
Important: When using OnPush, you must ensure that input references change when the data changes. Modifying objects in-place won’t trigger change detection.

Understanding OnPush Scenarios

If Angular handles an event within a component without OnPush, the framework executes change detection on the entire component tree, skipping OnPush subtrees that haven’t received new inputs.

Using trackBy for Performance

When rendering lists, use track to help Angular identify which items have changed:
@Component({
  selector: 'app-product-list',
  template: `
    @for (product of products(); track $index) {
      <div class="product">
        <h3>{{ product.name }}</h3>
        <p>{{ product.price }}</p>
      </div>
    }
  `
})
export class ProductListComponent {
  products = signal<Product[]>([]);
}
Why use trackBy? Without proper tracking, Angular recreates DOM elements when the array reference changes, even if the items are the same. This causes expensive DOM operations and loses component state.

Optimizing Computed Values

Use computed signals for expensive calculations to avoid recalculating on every change detection:
import { Component, signal, computed } from '@angular/core';

@Component({
  selector: 'app-dashboard',
  template: `
    <div class="stats">
      <p>Total Revenue: {{ totalRevenue() }}</p>
      <p>Average Order: {{ averageOrder() }}</p>
      <p>Top Product: {{ topProduct() }}</p>
    </div>
  `
})
export class DashboardComponent {
  orders = signal<Order[]>([]);
  
  // Computed values are cached and only recalculated when dependencies change
  protected totalRevenue = computed(() => 
    this.orders().reduce((sum, order) => sum + order.total, 0)
  );
  
  protected averageOrder = computed(() => {
    const orders = this.orders();
    return orders.length ? this.totalRevenue() / orders.length : 0;
  });
  
  protected topProduct = computed(() => {
    const productCounts = new Map<string, number>();
    this.orders().forEach(order => {
      order.items.forEach(item => {
        productCounts.set(item.product, (productCounts.get(item.product) || 0) + 1);
      });
    });
    return Array.from(productCounts.entries())
      .sort((a, b) => b[1] - a[1])[0]?.[0] || 'None';
  });
}

Zoneless Change Detection

Zoneless change detection removes ZoneJS overhead and triggers change detection only when signals or events indicate a change:
app.config.ts
import { ApplicationConfig, provideExperimentalZonelessChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideExperimentalZonelessChangeDetection(),  // Enable zoneless
    provideRouter(routes)
  ]
};
When to use: New applications (default in Angular v21+), or existing applications ready to migrate. Zoneless mode requires using signals for reactive state management.

Measuring Performance

Chrome DevTools Profiling

1

Open DevTools Performance Panel

Press F12 and navigate to the Performance tab
2

Start Recording

Click the record button and interact with your application
3

Analyze Angular Track

Look for the “Angular” track in the flame chart to see:
  • Component rendering times
  • Change detection cycles
  • Lifecycle hook execution
4

Identify Bottlenecks

Look for:
  • Long-running functions
  • Excessive change detection cycles
  • Slow component rendering

Angular DevTools

The Angular DevTools browser extension provides:

Component Inspector

Inspect component tree, view properties, and modify state in real-time

Profiler

Visualize change detection cycles and identify performance bottlenecks

Performance Checklist

  • Implement lazy loading for routes not needed on initial load
  • Use @defer blocks for heavy components below the fold
  • Optimize images with NgOptimizedImage directive
  • Consider server-side rendering for content-heavy pages
  • Enable production mode for deployments
  • Use OnPush change detection for presentational components
  • Always use track with unique identifiers in @for loops
  • Use computed signals for derived values
  • Avoid complex logic in templates
  • Consider zoneless change detection for new applications
  • Keep components focused on presentation
  • Move business logic to services
  • Use pure pipes for transformations
  • Avoid subscriptions in components (use async pipe or signals)
  • Unsubscribe from observables to prevent memory leaks
  • Analyze bundle size with ng build --stats-json
  • Remove unused dependencies
  • Use tree-shakable providers
  • Lazy load feature modules
  • Use dynamic imports for large libraries

What to Optimize First

Profile your application first using Chrome DevTools to identify specific bottlenecks. As a general starting point:

Slow Initial Load

  • Use @defer to split large components
  • Implement lazy loading for routes
  • Enable server-side rendering
  • Optimize images with NgOptimizedImage

Slow Interactions

  • Enable zoneless change detection
  • Look for slow computations in templates
  • Use OnPush change detection strategy
  • Add trackBy functions to lists
Pro Tip: Don’t optimize prematurely. Profile first, identify actual bottlenecks, then optimize those specific areas.

Next Steps

Learn about security best practices in Angular applications

Build docs developers (and LLMs) love