Migrating from AngularJS (1.x) to Angular (2+) brings numerous improvements, including better modularity, TypeScript support, and improved performance. However, many developers face performance issues after migration, such as:
- Slow page loads
- Unnecessary re-renders
- Memory leaks
- Inefficient change detection
This guide covers the common performance bottlenecks encountered after migration and provides best practices to optimize your Angular application.
1. Common Causes of Performance Issues After Migration
Issue | Cause |
---|---|
Slow Rendering | Heavy DOM manipulation and unnecessary change detection |
Memory Leaks | Subscriptions not unsubscribed properly |
Large Bundle Size | Not using lazy loading, tree-shaking, or Ahead-of-Time (AOT) compilation |
Slow HTTP Requests | Lack of caching and unnecessary API calls |
Inefficient Change Detection | Overuse of two-way binding ([(ngModel)] ) and not using OnPush strategy |
Unoptimized Loops | Not using trackBy in *ngFor loops |
Blocking Main Thread | Heavy synchronous operations and lack of Web Workers |
2. Optimizing Change Detection
Issue: Unnecessary Change Detection Calls
Angular uses a change detection mechanism to update the UI whenever data changes.
However, by default, Angular checks every component in the application, which can be slow.
Solution: Use OnPush
Change Detection Strategy
By default, Angular uses ChangeDetectionStrategy.Default, meaning it checks all components when data changes.
Instead, use ChangeDetectionStrategy.OnPush
to check components only when inputs change.
Before (Default Change Detection)
@Component({
selector: 'app-user',
template: `<p>{{ user.name }}</p>`
})
export class UserComponent {
@Input() user: any; // Triggers change detection always
}
After (Using OnPush
)
@Component({
selector: 'app-user',
template: `<p>{{ user.name }}</p>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserComponent {
@Input() user: any;
}
Now, Angular only updates this component when @Input()
changes, reducing unnecessary checks.
*3. Optimizing ngFor Loops with trackBy
Issue: Slow List Rendering
When using *ngFor
, Angular re-renders the entire list whenever data changes, leading to performance issues.
Solution: Use trackBy
Use trackBy
to tell Angular how to track items instead of re-rendering everything.
Before (Without trackBy)
<li *ngFor="let user of users">
{{ user.name }}
</li>
This will re-render all items if users
changes.
After (Using trackBy)
<li *ngFor="let user of users; trackBy: trackUser">
{{ user.name }}
</li>
trackUser(index: number, user: any) {
return user.id; // Track by unique ID instead of re-rendering all items
}
Now, Angular only updates the changed item instead of reloading everything.
4. Optimizing API Calls
Issue: Slow HTTP Requests & Repeated Calls
- Multiple API calls on each component reload
- No caching mechanism
- Long API response time
Solution: Use Caching & async
Pipe
Instead of making multiple API calls, store the results in a service.
Before (Unoptimized API Calls)
ngOnInit() {
this.http.get('https://api.example.com/users').subscribe(data => {
this.users = data;
});
}
Each time the component loads, a new API call is made.
After (Optimized with Caching)
private usersCache$: Observable<any>;
getUsers(): Observable<any> {
if (!this.usersCache$) {
this.usersCache$ = this.http.get('https://api.example.com/users').pipe(shareReplay(1));
}
return this.usersCache$;
}
- Now, API calls are cached, and data is fetched only once.
5. Reduce Bundle Size
Issue: Large Application Bundle
- Not using lazy loading
- No AOT (Ahead-of-Time) compilation
- Unused libraries included in the build
Solution: Use Lazy Loading & AOT
Enable Lazy Loading
Instead of loading all modules at once, use lazy loading:
const routes: Routes = [
{ path: 'dashboard', loadChildren: () => import('./dashboard.module').then(m => m.DashboardModule) }
];
Now, DashboardModule is only loaded when needed, improving the initial load time.
Enable AOT Compilation
In angular.json
, enable AOT:
"angularCompilerOptions": {
"enableIvy": true
}
AOT compiles Angular templates before runtime, making the app faster.
6. Memory Leak Prevention
Issue: Memory Leaks Due to Unsubscribed Observables
Memory leaks happen when subscriptions are not properly cleaned up.
Solution: Use takeUntil
or async
Pipe
Before (Leaking Memory)
this.subscription = this.http.get('https://api.example.com/data')
.subscribe(data => {
this.data = data;
});
- Problem: The subscription remains active even after the component is destroyed.
After (Using takeUntil)
private destroy$ = new Subject<void>();
this.http.get('https://api.example.com/data')
.pipe(takeUntil(this.destroy$))
.subscribe(data => {
this.data = data;
});
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
- Now, subscriptions are closed when the component is destroyed.
Alternative: Using async
Pipe
<p *ngIf="(data$ | async) as data">
{{ data.name }}
</p>
- The
async
pipe automatically unsubscribes when the component is destroyed.
7. Avoid Blocking the Main Thread
Issue: UI Freezes Due to Heavy Computations
- Running complex calculations synchronously blocks UI.
- Users experience lag when interacting with the page.
Solution: Use Web Workers
Move heavy computations to a Web Worker to prevent UI freezing.
Example
- Create a Web Worker (
worker.js
)
self.onmessage = function(event) {
const result = heavyComputation(event.data);
self.postMessage(result);
};
function heavyComputation(data) {
let sum = 0;
for (let i = 0; i < 10000000; i++) {
sum += i;
}
return sum;
}
- Use the Web Worker in Angular
const worker = new Worker(new URL('./worker.js', import.meta.url));
worker.postMessage(data);
worker.onmessage = ({ data }) => {
console.log('Result from worker:', data);
};
Now, complex tasks run in the background, improving UI performance.