Performance degradation after migrating to Angular

Loading

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

IssueCause
Slow RenderingHeavy DOM manipulation and unnecessary change detection
Memory LeaksSubscriptions not unsubscribed properly
Large Bundle SizeNot using lazy loading, tree-shaking, or Ahead-of-Time (AOT) compilation
Slow HTTP RequestsLack of caching and unnecessary API calls
Inefficient Change DetectionOveruse of two-way binding ([(ngModel)]) and not using OnPush strategy
Unoptimized LoopsNot using trackBy in *ngFor loops
Blocking Main ThreadHeavy 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

  1. 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;
}
  1. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *