It is important to tackle performance challenges to get performant enterprise Angular applications. As applications scale, multiple factors affect its performance. From poor component design to inefficient change detection, let’s break down the common challenges and their solutions with in-depth explanations and examples.
Common Performance Bottlenecks in Angular
- Inefficient Change Detection
- Large Bundle Size
- Memory Leaks
- Heavy Initial Load
- Too Many DOM Updates
- Excessive API Calls
- Unoptimized Angular Modules
- Inefficient RxJS Handling
Techniques to Handle Performance Bottlenecks
Use OnPush Change Detection Strategy
Angular uses Default Change Detection by default, which checks the entire component tree when any data changes. In large apps, this can be costly.
Solution: Use ChangeDetectionStrategy.OnPush
This tells Angular to check the component only when inputs change or an observable emits new data.
Example:
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
@Component({ selector: 'app-user-card', template: `<p>{{ user.name }}</p>`, changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserCardComponent { @Input() user: any;
}
Use immutability (e.g., user = { …user, name: ‘New Name’ }) so Angular detects the change.
Lazy Load Modules and Components
Load code only when needed instead of bundling everything in the initial load.
Solution: Use Angular’s lazy loading with routing or loadComponent.
Example (Routing):
const routes: Routes = [ { path: 'admin', loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule) }
];
Example (Standalone Component):
import { loadComponent } from '@angular/core';
{ path: 'product', loadComponent: () => import('./product/product.component').then(m => m.ProductComponent)
}
Tree-Shaking and Smaller Bundle Size
Large bundle sizes increase load time. Unused code should be removed.
Solution:
- Use –prod builds: ng build –configuration production
- Remove unused dependencies
- Prefer standalone components in Angular 15+ for more tree-shaking
Use trackBy with *ngFor
Without trackBy, Angular re-renders the whole list when anything changes.
Example:
<div *ngFor="let user of users; trackBy: trackById"> {{ user.name }}
</div>trackById(index: number, user: any) { return user.id;
}
Avoid Memory Leaks (unsubscribe properly)
Using observables without unsubscribing can lead to memory leaks.
Solution:
Use async pipe
Or manage subscriptions using takeUntil and Subject
Example with takeUntil:
private destroy$ = new Subject<void>();
ngOnInit() { this.apiService.getData() .pipe(takeUntil(this.destroy$)) .subscribe(data => this.data = data);
}
ngOnDestroy() { this.destroy$.next(); this.destroy$.complete();
}
Optimize Third-Party Libraries
Don’t import whole libraries—import only what you need.
Bad:
import * as _ from ‘lodash’;
Good:
import cloneDeep from ‘lodash/cloneDeep’;
Use Web Workers for Heavy Computation
Move CPU-intensive tasks off the main thread using Web Workers.
Example:
ng generate web-worker heavy-task
In the worker:
addEventListener('message', ({ data }) => { const result = heavyCalculation(data); postMessage(result);
});
In component:
const worker = new Worker(new URL('./heavy-task.worker', import.meta.url));
worker.postMessage(data);
worker.onmessage = ({ data }) => { this.result = data;
};
Use SSR or Pre-rendering for Faster FCP
Angular Universal allows server-side rendering (SSR), reducing Time to First Paint.
ng add @nguniversal/express-engine
Use Signals (Angular 17+) for Fine-grained Reactivity
Signals optimize change detection by precisely tracking reactive dependencies.
Example:
import { signal } from '@angular/core';
const count = signal(0);
count.update(n => n + 1);
This is more efficient than @Input or @Output for local state updates.
Profile and Debug
Use Chrome DevTools, Angular DevTools, and ng.profiler.timeChangeDetection() to find slow parts of your app.
Real-World Scenario
Problem:
A dashboard component renders 500 cards, each showing real-time data. The app slows down as more data updates arrive.
Optimization Steps:
- Use ChangeDetectionStrategy.OnPush in card components.
- Use trackBy in *ngFor.
- Replace frequent polling with WebSocket and RxJS distinctUntilChanged.
- Move cards into a lazily loaded module.
- Use requestAnimationFrame to batch DOM updates.
Preload card images or icons using a service.