← Back to Articles         
Improve Angular Application Performance

Improve Angular Application Performance

Published on 02 Oct 2026     10 min read Angular
Performance

How Do You Improve Angular Application Performance?

What is Angular Performance Optimization?

Angular performance optimization means reducing unnecessary work, loading less JavaScript/data, and rendering the UI efficiently so that the application:

  • Loads faster
  • Responds faster to user actions
  • Performs less unnecessary change detection
  • Uses less memory
  • Makes fewer unnecessary API calls
  • Handles large lists efficiently

The major areas are:

Angular Performance
        │
        ├── Change Detection
        ├── Signals
        ├── Lazy Loading
        ├── @defer
        ├── Efficient Rendering
        ├── RxJS / Subscription Management
        ├── HTTP / API Optimization
        ├── Bundle Optimization
        └── SSR / Hydration when appropriate

Full Simple Sample Program

This example combines several useful techniques:

  • OnPush
  • Signals
  • computed()
  • @for with track
  • Lazy-loaded routes

app.routes.ts

import { Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: '',
    loadComponent: () =>
      import('./home').then(m => m.Home)
  },
  {
    path: 'products',
    loadComponent: () =>
      import('./products').then(m => m.Products)
  }
];

products.ts

import {
  ChangeDetectionStrategy,
  Component,
  computed,
  signal
} from '@angular/core';

interface Product {
  id: number;
  name: string;
  price: number;
}

@Component({
  selector: 'app-products',
  standalone: true,
  templateUrl: './products.html',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class Products {

  products = signal<Product[]>([
    { id: 1, name: 'Laptop', price: 50000 },
    { id: 2, name: 'Mouse', price: 1000 },
    { id: 3, name: 'Keyboard', price: 2000 }
  ]);

  search = signal('');

  filteredProducts = computed(() => {

    const value = this.search().toLowerCase();

    return this.products().filter(product =>
      product.name.toLowerCase().includes(value)
    );

  });

  updateSearch(event: Event): void {

    const input = event.target as HTMLInputElement;

    this.search.set(input.value);
  }
}

products.html

<h2>Products</h2>

<input
  type="text"
  placeholder="Search product"
  (input)="updateSearch($event)"
/>

@for (product of filteredProducts(); track product.id) {

  <div>
    <h3>{{ product.name }}</h3>

    <p>
      Price: {{ product.price }}
    </p>
  </div>

}

Program Explanation

1. Lazy Loading

Instead of importing Products into the initial application bundle:

import { Products } from './products';

we use:

loadComponent: () =>
  import('./products').then(m => m.Products)

This allows Angular to load that component's code when the route is needed.

Application starts
       ↓
Initial code loaded
       ↓
User opens /products
       ↓
Products chunk loaded

This reduces the initial JavaScript payload.


2. Use OnPush

We configured:

changeDetection: ChangeDetectionStrategy.OnPush

OnPush allows Angular to skip component subtrees when they don't need checking.

Default
   ↓
Normal eager checking

OnPush
   ↓
Skip unaffected subtrees

This can be valuable in large component trees.


3. Use Signals

Instead of:

products: Product[] = [];
search = '';

we use:

products = signal<Product[]>([]);
search = signal('');

Signals provide Angular with explicit reactive dependencies.

Signal changes
      ↓
Angular knows its consumers
      ↓
Relevant views can be refreshed

Signals work particularly well with OnPush and zoneless-compatible Angular applications.


4. Use computed() for Derived Data

Instead of repeatedly putting filtering logic into the template, we create:

filteredProducts = computed(() => {

  const value = this.search().toLowerCase();

  return this.products().filter(product =>
    product.name.toLowerCase().includes(value)
  );

});

computed() tracks:

products
   +
search
   ↓
filteredProducts

It recalculates when its dependencies change and otherwise retains its computed value until invalidated.

This is useful for derived state.


5. Use track with @for

Consider:

@for (product of filteredProducts(); track product.id) {

The important part is:

track product.id

Angular can identify each item using its unique ID.

Suppose:

Product 1
Product 2
Product 3

becomes:

Product 1
Product 2
Product 3
Product 4

Tracking by ID helps Angular efficiently reconcile the collection rather than treating existing items as unrelated.

For modern Angular:

@for (product of products(); track product.id) {

For older *ngFor, you commonly used:

<div *ngFor="let product of products; trackBy: trackById">

Important Angular Performance Techniques

1. Use Lazy Loading

One of the most important optimizations.

Instead of loading the entire application immediately:

Login
Dashboard
Products
Orders
Reports
Admin
Settings

       ↓

Everything loaded initially

lazy load features:

Initial Load
   ↓
Only required code

User opens Reports
   ↓
Reports code loaded

Modern standalone Angular:

{
  path: 'products',
  loadComponent: () =>
    import('./products').then(m => m.Products)
}

For a group of routes:

{
  path: 'admin',
  loadChildren: () =>
    import('./admin/admin.routes')
      .then(m => m.ADMIN_ROUTES)
}

2. Use OnPush Where Appropriate

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush
})

This lets Angular skip unnecessary component subtree checks.

It works particularly well with:

OnPush
  +
Signals
  +
Immutable data
  +
AsyncPipe

3. Use Signals for Reactive State

Instead of relying on arbitrary mutable state:

count = 0;

consider:

count = signal(0);

Update:

this.count.update(value => value + 1);

Angular can track Signal consumers and refresh affected views.


4. Consider Zoneless Angular

Modern Angular supports running without Zone.js.

Instead of broadly relying on patched asynchronous APIs:

Async operation
     ↓
Zone.js
     ↓
Angular synchronization

zoneless Angular uses Angular-recognized notifications such as:

Signals
Template events
AsyncPipe
markForCheck()
Input changes

This can make change notification more explicit and reduce Zone.js-related overhead.

However, don't migrate an application to zoneless only because "zoneless must be faster." Verify application/library compatibility and measure the actual benefit.


5. Use @for with track

Avoid unstable identity.

Prefer:

@for (product of products(); track product.id) {

  <p>
    {{ product.name }}
  </p>

}

A stable unique key such as:

product.id

helps Angular efficiently update collections.


6. Use @defer

Modern Angular supports deferred loading of parts of a template.

For example:

<h1>Dashboard</h1>

@defer (on viewport) {

  <app-sales-chart />

} @placeholder {

  <p>Chart will load when needed...</p>

}

Conceptually:

Page loads
    ↓
Important content displayed
    ↓
User approaches chart
    ↓
Chart dependencies loaded

This is particularly useful for:

  • Charts
  • Heavy components
  • Content below the fold
  • Optional widgets
  • Large third-party libraries

7. Avoid Expensive Template Computations

Be careful with code such as:

<p>{{ calculateTotal() }}</p>

If calculateTotal() performs expensive work, repeated template evaluation can become costly.

For derived reactive state, prefer something like:

total = computed(() =>
  this.products()
    .reduce((sum, p) => sum + p.price, 0)
);

Template:

<p>{{ total() }}</p>

Now the calculation is tied to its reactive dependencies.


8. Use AsyncPipe

Instead of manually subscribing everywhere:

this.productService.getProducts()
  .subscribe(products => {
    this.products = products;
  });

when appropriate, expose the Observable:

products$ = this.productService.getProducts();

Template:

@for (product of products$ | async; track product.id) {
  {{ product.name }}
}

AsyncPipe manages subscription/unsubscription and integrates with Angular's change detection.


9. Prevent Subscription Memory Leaks

If you manually subscribe:

this.service.getData()
  .subscribe(...);

some long-lived Observables require cleanup.

Modern Angular provides:

takeUntilDestroyed()

Example:

import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

constructor() {

  this.service.notifications$
    .pipe(takeUntilDestroyed())
    .subscribe(value => {
      console.log(value);
    });

}

This helps prevent subscriptions from surviving longer than their component.


10. Optimize API Calls

Don't repeatedly call APIs unnecessarily.

Bad pattern:

Component loads
    ↓
GET products

Navigate away/back
    ↓
GET products

Another component
    ↓
GET same products

Depending on the data's freshness requirements, consider:

  • Client-side caching
  • Shared services
  • RxJS caching patterns
  • Server/API caching
  • HTTP caching

Also avoid downloading unnecessary data.

Instead of returning:

10,000 products

use server-side:

Pagination
Filtering
Sorting
Search

For example:

GET /api/products?page=1&pageSize=20

11. Debounce Search API Calls

Suppose a user types:

L
La
Lap
Lapt
Lapto
Laptop

Without debouncing, you might make six API requests.

Use RxJS operators such as:

debounceTime(300)
distinctUntilChanged()
switchMap(...)

Conceptually:

User typing rapidly
       ↓
Wait 300 ms
       ↓
Latest stable search value
       ↓
API request

switchMap() is especially useful because it can unsubscribe from a previous in-flight Observable when a newer search arrives.


12. Use Pagination or Virtual Scrolling

Avoid rendering thousands of DOM elements simultaneously.

Instead of:

10,000 records
      ↓
10,000 DOM rows

use:

Pagination

or, for suitable UI scenarios:

Virtual Scrolling

Virtual scrolling renders approximately what the user currently needs to see rather than every item at once.


13. Optimize Images

Images are frequently a major source of page weight.

Use Angular's image optimization facilities such as:

<img
  ngSrc="/images/product.jpg"
  width="400"
  height="300"
  alt="Product"
/>

with:

NgOptimizedImage

Also:

  • Use appropriately sized images
  • Compress images
  • Prefer modern formats when appropriate
  • Avoid downloading huge images only to display tiny thumbnails

14. Production Builds and Bundle Size

Production builds perform optimizations such as:

  • Minification
  • Tree shaking
  • Dead-code elimination/build optimizations
  • Code splitting where applicable

Build using the production configuration:

ng build --configuration production

Also inspect bundle sizes rather than assuming they are acceptable.

Large dependencies can significantly increase initial load time.


15. Avoid Unnecessary Third-Party Libraries

Suppose you need one simple utility but import a very large package.

Tiny feature
    ↓
Large dependency
    ↓
Larger JavaScript bundle

Before adding a package, check:

  • Bundle impact
  • Tree-shaking support
  • Whether Angular/browser APIs already solve the problem
  • Whether a smaller package is available

16. SSR and Hydration

For public-facing applications, Angular SSR can improve how quickly useful HTML becomes available and can benefit SEO.

Browser-only rendering

Browser
   ↓
Download JavaScript
   ↓
Execute Angular
   ↓
Render content

With SSR:

Server
   ↓
Initial HTML
   ↓
Browser displays content
   ↓
Angular hydrates
   ↓
Application becomes interactive

SSR is particularly relevant to:

  • Public websites
  • Article sites
  • E-commerce
  • SEO-sensitive pages

It is not automatically required for every internal dashboard or admin application.


Most Important Performance Techniques

For interview preparation, remember these 10:

1. Lazy load routes/features
2. Use OnPush appropriately
3. Use Signals/computed for reactive state
4. Track @for items using stable IDs
5. Use @defer for heavy/non-critical UI
6. Avoid expensive template computations
7. Manage RxJS subscriptions correctly
8. Reduce/debounce/cache API requests
9. Paginate or virtualize large lists
10. Optimize bundle size and images

Key Points

  • Measure first using browser/Angular profiling tools; don't optimize blindly.
  • Reduce the initial bundle size with lazy loading and deferred loading.
  • Reduce unnecessary view checking using OnPush and appropriate reactive state.
  • Use Signals and computed() for predictable reactive state and derived values.
  • Always use stable tracking for dynamic lists.
  • Don't render huge datasets at once.
  • Don't make unnecessary API requests.
  • Prevent long-lived RxJS subscription leaks.
  • Optimize images and third-party dependencies.
  • SSR/hydration can improve public-facing application startup characteristics, but should be chosen based on application requirements.

Interview Questions and Answers

1. How do you improve Angular application performance?

Use lazy loading, OnPush, Signals, stable @for tracking, @defer, efficient RxJS patterns, API optimization, pagination/virtual scrolling, image optimization, and bundle-size optimization.


2. How does OnPush improve performance?

OnPush allows Angular to skip component subtrees that don't require checking, reducing unnecessary change-detection work.

changeDetection: ChangeDetectionStrategy.OnPush

3. How do Signals help performance?

Signals provide reactive dependency tracking. Angular can know which views consume changed Signals and schedule the appropriate refresh work.

count = signal(0);

4. How does lazy loading improve performance?

Lazy loading keeps feature code out of the initial bundle and loads it when needed.

loadComponent: () =>
  import('./products').then(m => m.Products)

This can improve initial startup performance.


5. Why should we use track with @for?

It gives Angular stable identity for list items so Angular can efficiently reconcile additions, removals, moves, and updates.

@for (product of products(); track product.id) {
  {{ product.name }}
}

6. What is @defer?

@defer allows Angular to defer loading/rendering a section and its dependencies until a specified trigger occurs.

@defer (on viewport) {
  <app-chart />
}

It is useful for heavy or non-critical UI.


7. How do you optimize Angular search functionality?

Use RxJS operators such as:

debounceTime()
distinctUntilChanged()
switchMap()

This avoids making an API request for every keystroke and prevents stale search requests from dominating newer ones.


8. How do you handle thousands of records?

Prefer server-side pagination/filtering and, where appropriate, virtual scrolling rather than rendering thousands of DOM elements simultaneously.


9. Should we use zoneless Angular for performance?

Zoneless Angular can reduce reliance on broad Zone.js notifications and works well with explicit mechanisms such as Signals. However, migration should be based on compatibility and measured performance rather than assumption.


10. What is the most important rule for Angular performance optimization?

Measure the actual bottleneck first, then reduce unnecessary loading, rendering, change detection, computation, and network activity.

Interview Quick Answer

I improve Angular performance by reducing initial bundle size through lazy loading and @defer, reducing unnecessary change detection using OnPush and Signals, efficiently rendering lists with stable track keys, optimizing RxJS subscriptions and API calls, paginating large datasets, optimizing images, and monitoring production bundle/runtime performance.

← Back to Articles