To discuss any type of application performance, it’s important to define what we mean.
Being responsible for creating highly responsive interfaces requires that we care about performance. This means responding to user input and rendering in less than 17 milliseconds. Achieving this creates a smooth and seamless experience, in turn, increasing user confidence in your application.
We often write applications with little concern for the internal Angular operations. But, some situations need fine-tuning and a deeper understanding of what Angular does under the hood. Care is most often needed as data size or feature complexity grows. Data tables are a classic example. Often, a table that works well with small quantities of data begins lagging as the data size increases.
Knowing how your application’s events behave and how they interact with Angular is key to improving performance. As such, runtime performance in Angular ties to its change detection process, which includes three steps:
Let’s look at a few simple ways that we can improve each of these aspects.
Event handlers can exist in many locations within an Angular application. The most obvious examples are DOM and component event bindings. Designing these events to take as little time as possible ensures that change detection does not take more than 17 ms. If this target is not met the frame rate drops below 60 frames per second. This happens because Angular must wait for the callback to finish before change detection can continue and the update rendered.
Let’s examine a simple example of where an event binding may take longer to execute than anticipated. The following code contains a component (AppComponent) that responds to a click event. It does so by passing a value from the template into a service (ListService). The service in turn utilizes that value to filter a list. Finally, control returns to the click handler before completing. It isn’t until control returns from the service that change detection can continue. As the size of the list grows, the event’s performance will degrade.
<input type="text" [(ngMode)]="searchTerm">
<button (click)="update()">Search</button>
<ul>
<li *ngFor="let instructor of instructors">
{{ instructor }}
</li>
</ul>
import { Component } from '@angular/core';
@Component ({
selector: 'app-root',
templateUrl: 'app.component.html'
})
export class AppComponent {
public instructors = [];
searchTerm = '';
constructor(private ls: ListService) {
this.instructors = ls.getList(this.searchTerm);
}
// Executes when user clicks the "Search" button.
// Execution will flow into getList to perform the search.
// The results arrive back into the update method and assigned
// to the component.
// All of this happens within the same change detection cycle.
update() {
this.instructors = this.ls.getList(this.searchTerm);
}
}
import { Injectable } from '@angular/core';
@Injectable()
export class ListService {
private instructorList = [
'Paul Spears',
'Andrew Wiens',
'John Baur',
'Rachel Noccioli',
'Lance Finney',
];
getList(searchTerm: string) {
// Though this is a simple search or a small data set,
// As the data grows in length and complexity the performance of this method
// will likely degrade.
return this.instructorList
.filter(instructor => {
return instructor === searchTerm || searchTerm === '';
});
}
}
As demonstrated above, it is easy to forget that event handlers will often run code defined outside of the component definition. Identifying these lengthy processes allows them to be improved. This usually takes the form of choosing a more efficient algorithm. Alternatively, executing this process asynchronously allows change detection to complete while filtering finishes.
By default, components in an Angular application undergo change detection with nearly every user interaction. But, Angular allows you take control of this process. You can indicate to Angular if a component subtree is up to date and exclude it from change detection.
The first way to exclude a component subtree from change detection is by setting the changeDetection
property to ChangeDetectionStrategy.OnPush
in the @Component decorator. This tells Angular that the component only needs to be checked if an input has changed, and that all of the inputs can be considered immutable.
<!--
Since all bound values arrive as inputs
this component is suitable for use with OnPush
-->
<ul>
<li *ngFor="let instructor of instructors">
{{ instructor }}
</li>
</ul>
import { Component, Input, ChangeDetectionStrategy} from '@angular/core';
// Though this is a simple component, its template will only be checked for updates
// should a new list of instructors arrive.
// This is especially helpful when other portions of the application are undergoing
// frequent updates.
@Component({
selector: 'app-instructor-list',
templateUrl: './instructor-list.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class InstructorListComponent {
@Input() instructors = [];
}
At the price of a bit more complexity it is possible to increase the amount of control. For example, by injecting the ChangeDetectorRef service, you can inform Angular that a component should be detached from change detection. You can then manually take control of calling reattach
or detectChanges()
yourself, giving you full control of when and where a component subtree is checked.
import {
Component, Input, AfterViewInit,
ChangeDetectionStrategy, ChangeDetectorRef
} from '@angular/core';
@Component({
selector: 'app-instructor-list',
templateUrl: './instructor-list.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class InstructorListComponent implements AfterViewInit {
// Suppose this time the instructor list doesn't change after it arrives
@Input() instructors = [];
constructor(private cdr: ChangeDetectorRef) { }
// Wait until the view inits before disconnecting
ngAfterViewInit() {
// Since we know the list is not going to change
// let's request that this component not undergo change detection at all
this.crd.detach();
}
// Angular provides additional controls such as the following
// if the situation allows
// Request a single pass of change detection for the application
// this.cdr.markForCheck();
// Request a single pass of change detection for just this component
// this.cdr.detectChanges();
// Connect this component back to the change detection process
// this.cdr.reattach();
}
By default, when iterating over a list of objects, Angular will use object identity to determine if items are added, removed, or rearranged. This works well for most situations. However, with the introduction of immutable practices, changes to the list’s content generates new objects. In turn, ngFor will generate a new collection of DOM elements to be rendered. If the list is long or complex enough, this will increase the time it takes the browser to render updates. To mitigate this issue, it is possible to use trackBy to indicate how a change to an entry is determined.
<!--
As the references change ngFor will continuously regenerate the DOM
However, the presence of trackBy provides ngFor some help.
It modifies the behavior so that it compares new data to old based on
the return value of the supplied trackBy method.
This allows Angular to reduce the amount of DOM update needed
-->
<ul>
<li *ngFor="let instructor of instructorList: trackBy: trackByName" >
<span>Instructor Name {{ instructor.name }}</span>
</li>
</ul>
import { Component } from '@angular/core';
import { ListService } from '../list.service';
@Component({
selector: 'app-instructor-list',
templateUrl: './instructor-list.component.html'
})
export class InstructorListComponent {
instructorList = {name: string}[];
constructor(private ls: ListService){
// In this example let's assume the list service provides the
// instructors as a realtime list of filtered instructors.
// New updates are sent at regular intervals regardless of content change.
// As a result the object references change with each update.
ls.getList()
.subscribe(list => this.instructorList = list);
}
// Treat the instructor name as the unique identifier for the object
trackByName(index, instructor) {
return instructor.name;
}
}
Knowing where to begin when facing performance issues can be an intimidating task. Hopefully these practical examples show where performance issues can hide and help you get started. Although these tips are a great starting point, there are many more opportunities to increase performance.
#angular #javascript #node-js #web-development