Angular Runtime Optimization
Angular applications can sometimes become sluggish as they grow in size and complexity. Runtime optimization techniques help your application perform better while it's running in the browser. In this guide, we'll explore various ways to optimize Angular applications at runtime to provide users with a smooth, responsive experience.
Understanding Runtime Performance
Runtime performance refers to how well your application performs while users interact with it. Key metrics include:
- Time to Interactive (TTI) - How quickly users can interact with your app
- First Input Delay (FID) - How responsive the app is to user input
- Frame Rate - How smoothly animations and transitions run
- Memory Usage - How efficiently your app uses browser memory
Change Detection Optimization
Angular's change detection mechanism is powerful but can become a performance bottleneck in complex applications.
OnPush Change Detection Strategy
By default, Angular checks the entire component tree for changes whenever any data changes. The OnPush strategy makes components only check for updates when:
- Input references change
- An event originates from the component or its children
- Change detection is manually triggered
import { Component, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-user-list',
templateUrl: './user-list.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserListComponent {
@Input() users: User[];
// Component logic...
}
Manual Change Detection Control
For advanced scenarios, you can take manual control of change detection:
import { Component, ChangeDetectorRef } from '@angular/core';
@Component({
selector: 'app-data-grid',
templateUrl: './data-grid.component.html'
})
export class DataGridComponent {
private data: any[];
constructor(private cd: ChangeDetectorRef) {
// Detach change detector to stop automatic change detection
this.cd.detach();
// Later, when you want to check for changes:
this.updateData();
}
updateData() {
// Update your data
this.loadData();
// Manually trigger change detection
this.cd.detectChanges();
}
loadData() {
// Load data logic
}
}
Memory Management
Proper memory management prevents memory leaks that can slow down your application over time.
Unsubscribing from Observables
Always unsubscribe from observables when components are destroyed:
import { Component, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { DataService } from './data.service';
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html'
})
export class DashboardComponent implements OnDestroy {
private subscription: Subscription = new Subscription();
constructor(private dataService: DataService) {
// Add subscriptions to the main subscription
this.subscription.add(
this.dataService.getData().subscribe(data => {
// Handle data
})
);
}
ngOnDestroy() {
// Clean up all subscriptions when component is destroyed
this.subscription.unsubscribe();
}
}
Using the takeUntil Pattern
Another popular pattern for handling subscriptions:
import { Component, OnDestroy } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { DataService } from './data.service';
@Component({
selector: 'app-profile',
templateUrl: './profile.component.html'
})
export class ProfileComponent implements OnDestroy {
private destroy$ = new Subject<void>();
constructor(private dataService: DataService) {
this.dataService.getUserProfile()
.pipe(takeUntil(this.destroy$))
.subscribe(profile => {
// Handle profile data
});
}
ngOnDestroy() {
// Emit value to complete all subscriptions
this.destroy$.next();
this.destroy$.complete();
}
}
Rendering Optimization
Optimizing how Angular renders your UI can significantly improve performance.
Virtual Scrolling
For long lists, virtual scrolling renders only the items visible in the viewport:
// app.module.ts
import { ScrollingModule } from '@angular/cdk/scrolling';
@NgModule({
imports: [ScrollingModule]
})
export class AppModule { }
<!-- virtual-scroll.component.html -->
<cdk-virtual-scroll-viewport itemSize="50" class="viewport">
<div *cdkVirtualFor="let item of items" class="item">
{{ item.name }}
</div>
</cdk-virtual-scroll-viewport>
/* virtual-scroll.component.css */
.viewport {
height: 400px;
width: 100%;
}
.item {
height: 50px;
padding: 10px;
border-bottom: 1px solid #eee;
}
TrackBy Function
Help Angular identify which items have changed in a list to avoid unnecessary re-renders:
<div *ngFor="let user of users; trackBy: trackByUserId">
{{ user.name }}
</div>
trackByUserId(index: number, user: any): number {
return user.id;
}
Zone.js Optimization
Zone.js powers Angular's change detection. You can optimize how it works to improve performance.
NgZone runOutsideAngular
For computationally intensive operations or third-party code that doesn't need to trigger change detection:
import { Component, NgZone } from '@angular/core';
@Component({
selector: 'app-chart',
templateUrl: './chart.component.html'
})
export class ChartComponent {
constructor(private ngZone: NgZone) {
// Run third-party visualization library outside Angular's zone
this.ngZone.runOutsideAngular(() => {
// Heavy computation or third-party library initialization
this.initializeChart();
});
}
initializeChart() {
// Complex chart initialization that doesn't need change detection
// If we need to update Angular binding later
this.ngZone.run(() => {
this.chartLoaded = true;
});
}
}
Pure Pipes for Computed Values
Pure pipes cache their output based on input and are more efficient than methods in templates:
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'filter',
pure: true // Default is true
})
export class FilterPipe implements PipeTransform {
transform(items: any[], field: string, value: string): any[] {
if (!items) return [];
if (!value) return items;
return items.filter(item =>
item[field].toLowerCase().includes(value.toLowerCase())
);
}
}
<!-- Use in template -->
<div *ngFor="let item of items | filter:'name':searchTerm">
{{ item.name }}
</div>
Real-world Example: Optimizing a Dashboard
Let's look at a practical example of optimizing a dashboard component with multiple data sections:
import { Component, ChangeDetectionStrategy, OnDestroy } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { DashboardService } from './dashboard.service';
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DashboardComponent implements OnDestroy {
stats = { users: 0, revenue: 0, orders: 0 };
recentOrders: Order[] = [];
topProducts: Product[] = [];
isLoading = true;
private destroy$ = new Subject<void>();
constructor(
private dashboardService: DashboardService,
private ngZone: NgZone
) {
this.loadDashboardData();
}
loadDashboardData() {
// Load critical stats first
this.dashboardService.getDashboardStats()
.pipe(takeUntil(this.destroy$))
.subscribe(stats => {
this.stats = stats;
this.isLoading = false;
});
// Load non-critical data outside Angular zone
this.ngZone.runOutsideAngular(() => {
this.dashboardService.getRecentOrders()
.pipe(takeUntil(this.destroy$))
.subscribe(orders => {
this.recentOrders = orders;
// Update view when needed
this.ngZone.run(() => {});
});
this.dashboardService.getTopProducts()
.pipe(takeUntil(this.destroy$))
.subscribe(products => {
this.topProducts = products;
// Update view when needed
this.ngZone.run(() => {});
});
});
}
trackByOrderId(index: number, order: Order): number {
return order.id;
}
trackByProductId(index: number, product: Product): number {
return product.id;
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
<!-- dashboard.component.html -->
<div class="dashboard-container">
<div class="stats-container">
<app-stats-card
*ngIf="!isLoading"
[users]="stats.users"
[revenue]="stats.revenue"
[orders]="stats.orders">
</app-stats-card>
<app-skeleton *ngIf="isLoading"></app-skeleton>
</div>
<div class="recent-orders">
<h3>Recent Orders</h3>
<cdk-virtual-scroll-viewport itemSize="60" class="orders-viewport">
<div *cdkVirtualFor="let order of recentOrders; trackBy: trackByOrderId"
class="order-item">
{{ order.customerName }} - ${{ order.total | number:'1.2-2' }}
</div>
</cdk-virtual-scroll-viewport>
</div>
<div class="top-products">
<h3>Top Products</h3>
<div *ngFor="let product of topProducts | slice:0:5; trackBy: trackByProductId"
class="product-item">
{{ product.name }} - {{ product.salesCount }} sold
</div>
</div>
</div>
This example combines several optimization techniques:
- OnPush change detection
- Observable cleanup with takeUntil
- Virtual scrolling for order lists
- TrackBy functions for lists
- Running non-critical operations outside Angular's zone
- Loading critical data first for perceived performance
Summary
Runtime optimization in Angular is about finding bottlenecks and applying appropriate solutions. Key takeaways include:
- Use OnPush change detection for most components to reduce unnecessary rendering
- Always clean up observables to avoid memory leaks
- Use virtual scrolling for long lists
- Include trackBy functions for your ngFor directives
- Leverage pure pipes instead of methods in templates
- Run expensive operations outside Angular's zone when appropriate
By implementing these techniques, your Angular applications will be more responsive, use less memory, and provide a better overall user experience.
Additional Resources
Exercises
- Convert an existing component in your application to use OnPush change detection and measure the performance difference.
- Implement the takeUntil pattern for all observables in a component.
- Add virtual scrolling to a list that displays more than 100 items.
- Identify operations in your code that could benefit from running outside the Angular zone.
- Create a pure pipe to replace a computationally expensive method in your templates.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)