Angular Performance Tips
Performance optimization is crucial for creating responsive, user-friendly Angular applications. In this guide, we'll explore practical techniques to make your Angular apps faster and more efficient without compromising functionality.
Understanding Angular Performance
Before diving into specific optimizations, it's important to understand what affects Angular application performance:
- Initial Load Time: How quickly your app bootstraps and renders the first meaningful content
- Runtime Performance: How smoothly your app runs after loading (animations, user interactions)
- Memory Usage: How efficiently your app uses system resources
Essential Performance Techniques
1. Enable Production Mode
One of the simplest yet most impactful optimizations is enabling production mode in Angular.
// main.ts
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));
In production mode, Angular:
- Disables development-specific warnings
- Performs fewer change detection cycles
- Minimizes debugging information
2. Implement Lazy Loading
Lazy loading allows you to load features only when they're needed, significantly reducing initial load time.
Step 1: Structure your app into feature modules
// app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
const routes: Routes = [
{
path: 'dashboard',
loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule)
},
{
path: 'profile',
loadChildren: () => import('./profile/profile.module').then(m => m.ProfileModule)
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
Step 2: Create feature modules with their own routing
// dashboard/dashboard-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { DashboardComponent } from './dashboard.component';
const routes: Routes = [
{ path: '', component: DashboardComponent }
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class DashboardRoutingModule { }
// dashboard/dashboard.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DashboardRoutingModule } from './dashboard-routing.module';
import { DashboardComponent } from './dashboard.component';
@NgModule({
declarations: [DashboardComponent],
imports: [
CommonModule,
DashboardRoutingModule
]
})
export class DashboardModule { }
3. Optimize Change Detection
Angular's change detection mechanism can significantly impact performance. Here are ways to optimize it:
Use OnPush Change Detection Strategy
import { Component, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-user-card',
templateUrl: './user-card.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserCardComponent {
@Input() user: User;
// Component code
}
The OnPush strategy triggers change detection only when:
- Input properties change (reference changes)
- Events originate from the component or its children
- You manually trigger change detection
Detach Change Detection When Not Needed
For components that update infrequently:
import { Component, ChangeDetectorRef } from '@angular/core';
@Component({
selector: 'app-infrequent-updates',
template: '{{data}}'
})
export class InfrequentUpdatesComponent {
data: any;
constructor(private cd: ChangeDetectorRef) {
// Detach change detection
this.cd.detach();
// Update data every 10 seconds
setInterval(() => {
this.data = new Date().toLocaleTimeString();
// Manually detect changes when needed
this.cd.detectChanges();
}, 10000);
}
}
4. Track Items Using trackBy in ngFor
When using *ngFor
with arrays that change frequently, use trackBy
to improve rendering performance:
<div *ngFor="let item of items; trackBy: trackByFn">
{{ item.name }}
</div>
trackByFn(index: number, item: any): number {
return item.id; // Unique identifier
}
Without trackBy
, Angular recreates all DOM elements when the array reference changes. With trackBy
, it only updates the changed elements.
5. Use Pure Pipes Instead of Methods
Pipes in Angular are optimized for performance, especially pure pipes which implement pure functions.
Instead of:
<!-- Calls method on every change detection cycle -->
<div>{{ getFormattedDate(user.registrationDate) }}</div>
Use:
<!-- Pipe only recalculates when input changes -->
<div>{{ user.registrationDate | date:'short' }}</div>
Creating custom pure pipes:
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'fileSize',
pure: true // default is true
})
export class FileSizePipe implements PipeTransform {
transform(bytes: number): string {
if (bytes === 0) return '0 Bytes';
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return parseFloat((bytes / Math.pow(1024, i)).toFixed(2)) + ' ' + sizes[i];
}
}
Example usage:
<div>File size: {{ fileSize | fileSize }}</div>
6. Implement Virtual Scrolling
For long lists, virtual scrolling renders only visible items, drastically improving performance:
First, install the CDK:
npm install @angular/cdk
Add the ScrollingModule to your app:
import { ScrollingModule } from '@angular/cdk/scrolling';
@NgModule({
imports: [
ScrollingModule,
// other imports
]
})
export class AppModule { }
Implement virtual scrolling in your template:
<cdk-virtual-scroll-viewport itemSize="50" class="viewport">
<div *cdkVirtualFor="let item of items" class="item">
{{item.name}}
</div>
</cdk-virtual-scroll-viewport>
.viewport {
height: 400px;
width: 100%;
border: 1px solid black;
}
.item {
height: 50px;
padding: 10px;
box-sizing: border-box;
}
7. Preload Lazy-Loaded Modules
Improve user experience by preloading modules after the app loads:
// app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule, PreloadAllModules } from '@angular/router';
const routes: Routes = [
// your routes
];
@NgModule({
imports: [RouterModule.forRoot(routes, {
preloadingStrategy: PreloadAllModules
})],
exports: [RouterModule]
})
export class AppRoutingModule { }
For more control, create a custom preloading strategy:
import { Injectable } from '@angular/core';
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class SelectivePreloadingStrategy implements PreloadingStrategy {
preload(route: Route, load: () => Observable<any>): Observable<any> {
return route.data && route.data.preload ? load() : of(null);
}
}
Then use it in your routing module:
// app-routing.module.ts
import { SelectivePreloadingStrategy } from './selective-preloading-strategy';
@NgModule({
imports: [RouterModule.forRoot(routes, {
preloadingStrategy: SelectivePreloadingStrategy
})],
exports: [RouterModule]
})
export class AppRoutingModule { }
Mark routes for preloading:
const routes: Routes = [
{
path: 'dashboard',
loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule),
data: { preload: true }
}
];
Real-World Example: Optimizing a Dashboard
Let's apply these techniques to a dashboard application with multiple components and data-heavy views:
// dashboard.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ScrollingModule } from '@angular/cdk/scrolling';
import { DashboardComponent } from './dashboard.component';
import { ChartComponent } from './chart/chart.component';
import { DataTableComponent } from './data-table/data-table.component';
import { FileSizePipe } from './pipes/file-size.pipe';
import { DashboardRoutingModule } from './dashboard-routing.module';
@NgModule({
declarations: [
DashboardComponent,
ChartComponent,
DataTableComponent,
FileSizePipe
],
imports: [
CommonModule,
ScrollingModule,
DashboardRoutingModule
]
})
export class DashboardModule { }
// data-table.component.ts
import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-data-table',
template: `
<cdk-virtual-scroll-viewport itemSize="50" class="viewport">
<div *cdkVirtualFor="let row of data; trackBy: trackById" class="row">
<div>{{row.id}}</div>
<div>{{row.name}}</div>
<div>{{row.size | fileSize}}</div>
<div>{{row.lastModified | date:'short'}}</div>
</div>
</cdk-virtual-scroll-viewport>
`,
styles: [`
.viewport { height: 400px; width: 100%; }
.row { display: flex; height: 50px; align-items: center; }
.row div { flex: 1; padding: 0 10px; }
`],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DataTableComponent {
@Input() data: any[];
trackById(index: number, item: any): number {
return item.id;
}
}
// chart.component.ts
import { Component, Input, OnChanges, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-chart',
template: '<div id="chart-container" style="height: 300px;"></div>',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChartComponent implements OnChanges {
@Input() chartData: any;
ngOnChanges() {
// Only re-render chart when input data changes
this.renderChart();
}
private renderChart() {
// Chart rendering logic
console.log('Rendering chart with data:', this.chartData);
}
}
// dashboard.component.ts
import { Component, OnInit } from '@angular/core';
import { DashboardService } from './dashboard.service';
@Component({
selector: 'app-dashboard',
template: `
<div class="dashboard">
<app-chart [chartData]="chartData"></app-chart>
<app-data-table [data]="tableData"></app-data-table>
</div>
`
})
export class DashboardComponent implements OnInit {
chartData: any;
tableData: any[] = [];
constructor(private dashboardService: DashboardService) {}
ngOnInit() {
this.dashboardService.getChartData().subscribe(data => {
this.chartData = data;
});
this.dashboardService.getTableData().subscribe(data => {
this.tableData = data;
});
}
}
Summary
Implementing these Angular performance tips can significantly improve your application's speed and user experience:
- Enable Production Mode: Reduces overhead and optimizes for performance
- Implement Lazy Loading: Loads features only when needed
- Optimize Change Detection: Use OnPush and manual change detection strategically
- Use trackBy with ngFor: Reduces DOM operations
- Prefer Pure Pipes over Methods: Improves rendering performance
- Implement Virtual Scrolling: Efficiently renders large lists
- Use Strategic Module Preloading: Improves perceived performance
Remember that performance optimization should be approached systematically:
- Measure first to identify bottlenecks (use Angular DevTools or Chrome Performance tools)
- Optimize the most impactful areas first
- Verify improvements with measurements
Additional Resources
- Angular Performance Checklist
- Angular DevTools
- Web.dev Performance Guide
- Chrome DevTools Performance Analysis
Exercises
-
Analyze an Application: Use Angular DevTools to analyze an existing application and identify performance bottlenecks.
-
Implement Lazy Loading: Convert an eager-loaded module in your application to a lazy-loaded one and measure the impact.
-
Optimize Change Detection: Identify components that don't need frequent updates and implement OnPush change detection.
-
Virtual Scrolling: Implement virtual scrolling for a list with more than 100 items and compare performance against a standard ngFor implementation.
-
Build Your Own Preloading Strategy: Create a custom preloading strategy that preloads modules based on user behavior patterns.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)