How to optimize Angular performance

Optimizing Angular performance involves change detection strategies, lazy loading, bundle optimization, and runtime performance improvements. As the creator of CoreUI with over 12 years of Angular experience since 2014, I’ve optimized numerous enterprise applications for production performance. Angular provides powerful optimization tools including OnPush change detection, lazy loading, and AOT compilation for faster applications. This approach ensures smooth user experience even in complex, data-heavy applications.

Use OnPush change detection, lazy loading, trackBy, AOT compilation, and bundle optimization to maximize Angular performance.

OnPush change detection:

// user-list.component.ts
import { Component, Input, ChangeDetectionStrategy } from '@angular/core'

interface User {
  id: number
  name: string
  email: string
}

@Component({
  selector: 'app-user-list',
  template: `
    <div *ngFor="let user of users; trackBy: trackById" class="user">
      <h3>{{ user.name }}</h3>
      <p>{{ user.email }}</p>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserListComponent {
  @Input() users: User[] = []

  trackById(index: number, user: User): number {
    return user.id
  }
}

// parent.component.ts
@Component({
  template: `<app-user-list [users]="users"></app-user-list>`
})
export class ParentComponent {
  users: User[] = []

  addUser(user: User): void {
    // Create new array reference for OnPush detection
    this.users = [...this.users, user]
  }
}

Lazy loading modules:

// app-routing.module.ts
import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'

const routes: Routes = [
  {
    path: 'dashboard',
    loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule)
  },
  {
    path: 'users',
    loadChildren: () => import('./users/users.module').then(m => m.UsersModule)
  },
  {
    path: 'settings',
    loadChildren: () => import('./settings/settings.module').then(m => m.SettingsModule)
  }
]

@NgModule({
  imports: [RouterModule.forRoot(routes, {
    preloadingStrategy: PreloadAllModules // or SelectivePreloadingStrategy
  })],
  exports: [RouterModule]
})
export class AppRoutingModule {}

Virtual scrolling for large lists:

// virtual-scroll.component.ts
import { Component } from '@angular/core'
import { ScrollingModule } from '@angular/cdk/scrolling'

@Component({
  selector: 'app-virtual-scroll',
  standalone: true,
  imports: [ScrollingModule],
  template: `
    <cdk-virtual-scroll-viewport itemSize="50" class="viewport">
      <div *cdkVirtualFor="let item of items; trackBy: trackById" class="item">
        {{ item.name }}
      </div>
    </cdk-virtual-scroll-viewport>
  `,
  styles: [`
    .viewport {
      height: 500px;
      width: 100%;
    }
    .item {
      height: 50px;
    }
  `]
})
export class VirtualScrollComponent {
  items = Array.from({ length: 10000 }, (_, i) => ({
    id: i,
    name: `Item ${i}`
  }))

  trackById(index: number, item: any): number {
    return item.id
  }
}

Pure pipes for expensive computations:

// filter.pipe.ts
import { Pipe, PipeTransform } from '@angular/core'

@Pipe({
  name: 'filter',
  pure: true // Default, but explicit for clarity
})
export class FilterPipe implements PipeTransform {
  transform(items: any[], searchTerm: string): any[] {
    if (!items || !searchTerm) {
      return items
    }
    return items.filter(item =>
      item.name.toLowerCase().includes(searchTerm.toLowerCase())
    )
  }
}

// Usage in component
@Component({
  template: `
    <input [(ngModel)]="searchTerm" placeholder="Search">
    <div *ngFor="let item of items | filter:searchTerm; trackBy: trackById">
      {{ item.name }}
    </div>
  `
})
export class ListComponent {
  items = []
  searchTerm = ''

  trackById(index: number, item: any): number {
    return item.id
  }
}

Detach change detection for heavy operations:

// heavy-computation.component.ts
import { Component, ChangeDetectorRef, OnDestroy } from '@angular/core'

@Component({
  selector: 'app-heavy-computation',
  template: `
    <div>{{ result }}</div>
    <button (click)="startComputation()">Start</button>
  `
})
export class HeavyComputationComponent implements OnDestroy {
  result = 0

  constructor(private cdr: ChangeDetectorRef) {}

  startComputation(): void {
    // Detach change detection during heavy operation
    this.cdr.detach()

    this.performHeavyComputation().then(result => {
      this.result = result
      // Reattach and trigger detection
      this.cdr.reattach()
      this.cdr.detectChanges()
    })
  }

  async performHeavyComputation(): Promise<number> {
    // Simulate heavy computation
    return new Promise(resolve => {
      setTimeout(() => resolve(Math.random()), 2000)
    })
  }

  ngOnDestroy(): void {
    this.cdr.detach()
  }
}

Production build optimization:

// angular.json
{
  "projects": {
    "app": {
      "architect": {
        "build": {
          "configurations": {
            "production": {
              "optimization": true,
              "outputHashing": "all",
              "sourceMap": false,
              "namedChunks": false,
              "aot": true,
              "extractLicenses": true,
              "vendorChunk": false,
              "buildOptimizer": true,
              "budgets": [
                {
                  "type": "initial",
                  "maximumWarning": "500kb",
                  "maximumError": "1mb"
                },
                {
                  "type": "anyComponentStyle",
                  "maximumWarning": "2kb",
                  "maximumError": "4kb"
                }
              ]
            }
          }
        }
      }
    }
  }
}

Optimize observables:

// optimized.component.ts
import { Component, OnDestroy } from '@angular/core'
import { Subject, takeUntil } from 'rxjs'

@Component({
  selector: 'app-optimized',
  template: `<div>{{ data }}</div>`
})
export class OptimizedComponent implements OnDestroy {
  data = ''
  private destroy$ = new Subject<void>()

  constructor(private dataService: DataService) {
    // Unsubscribe automatically
    this.dataService.getData()
      .pipe(takeUntil(this.destroy$))
      .subscribe(data => this.data = data)
  }

  ngOnDestroy(): void {
    this.destroy$.next()
    this.destroy$.complete()
  }
}

Best Practice Note

Enable production mode with AOT compilation for smaller bundles and faster runtime. Use OnPush change detection on all components that receive immutable inputs. Implement trackBy for all ngFor loops. Lazy load feature modules to reduce initial bundle size. Use pure pipes for expensive transformations. Implement virtual scrolling for large lists. Detach change detection during heavy operations. Unsubscribe from observables in ngOnDestroy. This is how we optimize CoreUI Angular components—OnPush everywhere, lazy loading, trackBy on lists, and production builds reducing bundle size by 40-60% while improving runtime performance significantly.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author

Subscribe to our newsletter
Get early information about new products, product updates and blog posts.

Answers by CoreUI Core Team