Skip to main content

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:

  1. Initial Load Time: How quickly your app bootstraps and renders the first meaningful content
  2. Runtime Performance: How smoothly your app runs after loading (animations, user interactions)
  3. 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.

typescript
// 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​

typescript
// 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​

typescript
// 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 { }
typescript
// 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​

typescript
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:

typescript
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:

html
<div *ngFor="let item of items; trackBy: trackByFn">
{{ item.name }}
</div>
typescript
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:

html
<!-- Calls method on every change detection cycle -->
<div>{{ getFormattedDate(user.registrationDate) }}</div>

Use:

html
<!-- Pipe only recalculates when input changes -->
<div>{{ user.registrationDate | date:'short' }}</div>

Creating custom pure pipes:

typescript
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:

html
<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:

bash
npm install @angular/cdk

Add the ScrollingModule to your app:

typescript
import { ScrollingModule } from '@angular/cdk/scrolling';

@NgModule({
imports: [
ScrollingModule,
// other imports
]
})
export class AppModule { }

Implement virtual scrolling in your template:

html
<cdk-virtual-scroll-viewport itemSize="50" class="viewport">
<div *cdkVirtualFor="let item of items" class="item">
{{item.name}}
</div>
</cdk-virtual-scroll-viewport>
css
.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:

typescript
// 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:

typescript
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:

typescript
// 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:

typescript
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:

typescript
// 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 { }
typescript
// 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;
}
}
typescript
// 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);
}
}
typescript
// 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:

  1. Enable Production Mode: Reduces overhead and optimizes for performance
  2. Implement Lazy Loading: Loads features only when needed
  3. Optimize Change Detection: Use OnPush and manual change detection strategically
  4. Use trackBy with ngFor: Reduces DOM operations
  5. Prefer Pure Pipes over Methods: Improves rendering performance
  6. Implement Virtual Scrolling: Efficiently renders large lists
  7. 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​

Exercises​

  1. Analyze an Application: Use Angular DevTools to analyze an existing application and identify performance bottlenecks.

  2. Implement Lazy Loading: Convert an eager-loaded module in your application to a lazy-loaded one and measure the impact.

  3. Optimize Change Detection: Identify components that don't need frequent updates and implement OnPush change detection.

  4. Virtual Scrolling: Implement virtual scrolling for a list with more than 100 items and compare performance against a standard ngFor implementation.

  5. 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 feedback@compilenrun.com. I’d greatly appreciate your feedback! :)