Why Angular change detection can get expensive
Angular periodically runs its change detection mechanism so that updates to the data model are reflected in the view. Change detection can be triggered manually or through asynchronous events such as user interactions or XHR completions. It is a powerful tool, but when it runs frequently it can trigger a lot of computations and block the main browser thread.
To understand how to control this, consider a sample application that lists employees from two departments: sales and R&D. The app has a root AppComponent and two instances of EmployeeListComponent—one per department. Each employee has a name and a numeric value. The numeric value is passed to a business calculation (a fibonacci function) and the result is rendered on screen.
Here is the template for AppComponent:
<app-employee-list
[data]="salesList"
department="Sales"
(add)="add(salesList, $event)"
(remove)="remove(salesList, $event)"
></app-employee-list>
<app-employee-list
[data]="rndList"
department="R&D"
(add)="add(rndList, $event)"
(remove)="remove(rndList, $event)"
></app-employee-list>
And here is EmployeeListComponent:
const fibonacci = (num: number): number => {
if (num === 1 || num === 2) {
return 1;
}
return fibonacci(num - 1) + fibonacci(num - 2);
};
@Component(...)
export class EmployeeListComponent {
@Input() data: EmployeeData[];
@Input() department: string;
@Output() remove = new EventEmitter<EmployeeData>();
@Output() add = new EventEmitter<string>();
label: string;
handleKey(event: any) {
if (event.keyCode === 13) {
this.add.emit(this.label);
this.label = '';
}
}
calculate(num: number) {
return fibonacci(num);
}
}
The component accepts a list of employees and a department name as inputs, and emits outputs when the user adds or removes an employee. The template iterates over the employees and includes an ngModel directive for two-way data binding:
<h1 title="Department">{{ department }}</h1>
<mat-form-field>
<input placeholder="Enter name here" matInput type="text" [(ngModel)]="label" (keydown)="handleKey($event)">
</mat-form-field>
<mat-list>
<mat-list-item *ngFor="let item of data">
<h3 matLine title="Name">
{{ item.label }}
</h3>
<md-chip title="Score" class="mat-chip mat-primary mat-chip-selected" color="primary" selected="true">
{{ calculate(item.num) }}
</md-chip>
</mat-list-item>
</mat-list>
With two EmployeeListComponent instances, the app forms a component tree rooted at AppComponent:
When the user types a name in an input box in one EmployeeListComponent, Angular triggers change detection for the entire tree starting from AppComponent. While the user is typing, Angular repeatedly recalculates the numeric values for each employee to verify they haven’t changed since the last check. With many employees in the list, this process can block the browser’s UI thread and cause frame drops.
To confirm the slowdown comes from the fibonacci function, open the non-optimized version of the project on StackBlitz, then profile with Chrome DevTools: press Control+Shift+J (or Command+Option+J on Mac), click the Performance tab, click Record, type in one of the text boxes, and click Record again to stop.
Skip component subtrees with OnPush
When the user is typing in the sales department’s input, the R&D department data isn’t changing—so there’s no reason to run change detection on that component. To prevent the R&D instance from triggering change detection, set the changeDetectionStrategy of EmployeeListComponent to OnPush:
import { ChangeDetectionStrategy, ... } from '@angular/core';
@Component({
selector: 'app-employee-list',
template: `...`,
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrls: ['employee-list.component.css']
})
export class EmployeeListComponent {...}
With this strategy, change detection runs only for the department whose input the user is editing:
The optimized version of the app is available in the onpush branch of the repository, and you can try it on StackBlitz.
Cache heavy computation with pure pipes
Even with OnPush in place, Angular still recalculates the numeric value for all employees in a department when the user types in the corresponding input. To improve this, move the heavy calculation into a pure pipe. Both pure and impure pipes accept inputs and return results used in a template, but a pure pipe recalculates its result only when it receives a different input than its previous invocation—determined by a reference check.
Here is the business calculation extracted into a CalculatePipe whose transform method invokes the fibonacci function:
import { Pipe, PipeTransform } from '@angular/core';
const fibonacci = (num: number): number => {
if (num === 1 || num === 2) {
return 1;
}
return fibonacci(num - 1) + fibonacci(num - 2);
};
@Pipe({
name: 'calculate'
})
export class CalculatePipe implements PipeTransform {
transform(val: number) {
return fibonacci(val);
}
}
The pipe is pure by default (Angular considers all pipes pure unless specified otherwise). Finally, update the template expression in EmployeeListComponent to use the pipe:
<mat-chip-list>
<md-chip>
{{ item.num | calculate }}
</md-chip>
</mat-chip-list>
Now typing in the input won’t trigger recalculations for individual employees unless an employee’s numeric value actually changes.
Summary
When an Angular app runs slowly at runtime:
- Profile with Chrome DevTools to locate the source of the slowdown.
- Use the
OnPushchange detection strategy to prune component subtrees that don’t need checking. - Move heavy computations into pure pipes so the framework can cache computed values between calls.



