This article describes my experience tuning up angular application using a few common and a few lesser known techniques related to change detection. Doing so helped me learn how change detection in Angular works under the hood.

When developing web applications using Angular framework we rarely think about performance implications. Angular is designed to be blazing fast and in general relieves us from performance debugging headaches so we end up losing our grip on “performance first” development practices. As our application grows and we add layers and layers of business logic we may face the degradation of performance metrics and ultimately user experience overall. I will mostly concentrate on runtime performance and my attempts to improve it in a production web application.

Performance plays a major role in the success of any online venture. For example, Pinterest increased search engine traffic and sign-ups by 15% when they reduced perceived wait times by 40%.

A very very busy app

Let me explain the problem I was having. The website  I am working on now has interactive interface with a lot of requests going back and forth. We use WebSockets to receive push updates from other clients which go to broker first and then to browser. Its nature is nothing like a traditional web application you can imagine. It does not have many screens — rather few where one user creates a request and other receives it in real time and acts upon it which results in another update to the request initiator. This process may send the request back and forth a few times and eventually transaction results in the end state.

Like any other Angular application we have a structure which is considered to be pretty orthodox — 2 modules with its individual routes where each is lazy loaded and each has a tree of components. The component which is causing performance issues is a list built with ngFor directive. Each item in the list contained within the transaction component. Here is the piece of code from list.component.html:

...
<tr [ngClass]="getRowClass(row)"
  #transaction
  (click)="lock(row, $event)"
  [model]="row"
  ngFor="let row of rows; trackBy:rowIdentity">
</tr>
...

Each transaction component’s template is essentially a bunch of td containers with some business data: creation date, status, owner id etc. There is two types of statuses — active and completed. Transactions are added to the list dynamically via WebSocket and pushed to the bottom. User either accepts the transaction or rejects it, then moves on to the next one. Look at these screenshots I have made for you to better understand what I am talking about:

There are two transactions in the list — one completed and one active. User can click Submit or Pass for accepted or cancelled transaction. This exact action is prone to performance issues when list grows. Consider this screenshot:

As you can see list grows and all completed rows are stacked up at the top. Pretty straight forward. Every time use performs an action we change the row’s background color and add some other visual indication that the row becomes inactive(completed). The larger the list — the longer it takes to update the row with appropriate styles. Let’s try to figure out why this is the case.

Analyze, then Optimize

When it comes to benchmarking the JavaScript application things get not as clear as it could be. Due to JavaScript’s asynchronous nature profiling the performance may be an overwhelming task to deal with. With performance.measure Web API’s one can analyze how fast or slow code runs to some extent, but to get a holistic and complete picture  is quite an exercise.

This is where Performance Analyzer from Chrome Developer Tools comes in handy. It is de facto the most comprehensive and complete instrument for measuring web application’s both network and runtime performance. I won’t lie it took me some time to get around it. The amount of data it spits out is enormous and it may be challenging at first to consume and make sense of it. For instance, I needed to understand what was slowing down the system when the list was growing. So I decided to take some measurements.

It is also worth noting that the slowdown is hardly noticeable on modern powerful machines like the one I have (2.6 GHz Intel Core i7 16Gb DDR4). One of our clients had an older computer where it was easier to replicate this problem. So for me in order to emulate this environment I needed to throttle CPU power — I made a 6x slowdown. And then I decided to run two sets of tests. I wanted to analyze how long does it take to complete or reject a trade on a list with 1 transaction, 5 transactions and 10 transactions for both real CPU and throttled CPU. I performed 10 tests for each scenario and calculated a mean for each scenario to see the averaged result. Here are the metrics I got:

Time in milliseconds is the actual scripting time — indicates how long javascript code runs from the event of user clicking on the button and until the row is updated with appropriate css styles (during this process application makes a REST call to the server, gets a response and paints the row). It is clear (or not?) that the more rows in the table — the longer it takes to process an individual transaction. And the tests were taken with only one active transaction at a time meaning that the rest of them were already in the completed state.

So why does it take longer? What is going on here?

Let’s review the most interesting parts of the list.component.ts file:

...
@Component({
  selector: 'list',
  templateUrl: './list.component.html',
  styleUrls: ['./list.component.scss']
})

export class ListComponent implements OnChanges {

  @Input() rows: Transaction[];

...

  public getRowClass(row: Transaction) {
    return {
      'active': this.trService.isActive(row),
      'rejected': this.trService.isRejected(row),
      'accepted': this.trService.isAccepted(row)
    };
  }

...

}

OK, turns out there are few things which can be improved about this code. Every experienced Angular developer will point out that ChangeDetectionStrategy should be set to onPush . This technique is well described pretty much in every article about Angular Performance and is fairly easy to implement. If you are not familiar with it please read this great article by Max Koretskyi aka Wizard. It goes in depth of Angular Change Detection Mechanism.

All I needed to do is set changeDetection: ChangeDetectionStrategy.onPush in Component decorator. Well, as we know when ChangeDetectionStrategy is set to onPush then component’s will be checked only if its Inputs references are changed (also Event which originates from component or its children will also trigger change detection) therefore if you mutate the Inputs for your components then you will need to change all those instances to be deep copied (copied by value). Check by reference is much faster therefore this is why it improves the overall performance.

Another improvement which I made was the caching of the getRowClass output. This method is run every time component was checked which is happening on every Change Detection run for every element in the list. I changed it to cache the classes for each row identifier only when any of the rows are changed. Here is the code:

...
@Component({
  selector: 'list',
  templateUrl: './list.component.html',
  styleUrls: ['./list.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})

export class ListComponent implements OnChanges {

  @Input() rows: Transaction[];
  
  private _rows: {[key: string]: Transaction} = {};
  public rowClasses: {[key: string]: any} = {};

...

  ngOnChanges() {
    this.rows.forEach((row) => {
      if (!this._rows[row.id] || this._rows[row.id].version !== row.version) {
        this._rows[row.id] = row;
        this.setRowClasses(row);
      }
    });
  }
  
  private setRowClasses(row) {
    this.rowClasses[row.id] = {
      'active': this.trService.isActive(row),
      'rejected': this.trService.isRejected(row),
      'accepted': this.trService.isAccepted(row)
    };
  }
  
  public getRowClasses(row: Transaction) {
    return this.rowClasses[row.id];
  }
  
...

}

Alright, list looks better after these simple modifications. Let’s look at transaction.component.ts now. Again, no onPush detection strategy set — adding that. Another very important behavior worth pointing out — every child component is being checked every time its sibling is updated: this is not desired for rows which are already in the completed state, they are not concerned about any further updates, they should not be running any code inside of them. We may ask why then you keep those rows on the screen, just clear them out — the answer we do have a way to clear those completed rows but users want to keep them on the screen for some time after they are completed for accounting purposes. That is why it is important to optimize the list in the best way possible. There is a solution. Why don’t we disable the change detection for the component once it becomes inactive? ChangeDetectionRef class API’s has method detach which detaches component’s view from the change detection tree so that next time if parent component triggers change detection it won’t affect this child component. Let’s look at the code:

...

@Component({
  selector: 'transaction',
  templateUrl: './transaction.html',
  styleUrls: ['./transaction.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TransactionComponent implements OnChanges {

  @Input() model: Transaction;
  
  ...
  
  ngOnChanges() {
    if (this.trService.isInactive(this.model)) {
      this.ref.detach();
    }
  }
  
  ...
  
}

OK, we are moving in the right direction. After this round of changes I ran the performance tests again and here were the results:

Alright, now tests run almost twice as fast — this is a significant improvement. But we can do better. I want to go over ngFor directive in order to determine how we can improve its performance. We know that this directive helps us to build collections. We place it on the container which becomes a parent for cloned templates ngFor creates for every element in the collection. This directive runs a check using IterableDiffer interface to identify the changes in the collection between the change detection runs. The result of it is an object of IterableChanges which then are applied to update the view. For a much deeper dive in ngFor internals read this great article.

What I am trying to say is next: we should not run a Differ on a list with items which never change. Our completed rows are evaluated every change detection run which is inefficient. The solution would be to move all these rows out of the main collection so that only active transactions are being evaluated for possible changes. As a result I moved all completed transactions to another completedTransactions array and looped over it in the template just the same way using ngFor . After this step I ran the performance tests again:

Finally! It does not matter anymore how big the collection is. It takes the same time to process an update on one active transaction and the list of 10 transactions with 9 of them being inactive which logically makes sense. (83 ms on throttled CPU for 1 row and 85 ms for 10 rows). This is much much better.

To sum up, here is the list of changes I've applied:

  • updated change detection strategy to onPush for both parent and child components;
  • cached the most expensive methods for child components so we don’t recalculate the unchanged output every change detection run;
  • disabled change detection for the elements which don’t change anymore;
  • moved all completed elements to another collection so that  ngFor internally runs faster.

Other potential improvements?

I personally did not try to implement this but virtual scrolling in my opinion is worth the shot. In a nutshell it is a way to reduce the number of rows on user’s view port which in result improves performance: less rows -> more performant application. I know that ag-Grid, a very powerful Angular datagrid, uses this technique under the hood to process hundreds of thousands records without any noticeable performance hit.

Another improvement would be to move all housekeeping logic to Web Workers — staff like server logging, Web Socket heartbeats etc. Great article about using Web Workers can be found here — Improve Performance with Web Workers.

That's it. Thanks for reading.