Manual change detection in Angular for performance

There are special use cases where you might need to customize change detection in Angular applications for performance reasons. The majority of the time, default change detection is the correct way to go. I’ve worked with several customers recently who were using very large datasets within Angular 8 components and this caused some interesting UI slowdowns as a side-effect.

Fortunately, and unsurprisingly, Angular does allow you modify change detection, for example ChangeDetectionStrategy.OnPush and ChangeDetectorRef. In both cases that I worked on, my recommendation was to lean towards ChangeDetectorRef in order to provide granular, explicit control over when a component updated to reduce performance impacts on the UI.

In one case, the change detection needed to be slowed down and it was suitable to run it on a timer loop. Here’s the pseudo-code, and there are plenty of similar examples on the internet including in the Angular documentation:

import { Component, ChangeDetectorRef, . . .  } from '@angular/core';

constructor(private changeDetector: ChangeDetectorRef) {
    changeDetector.detach();
    setInterval(() => {
      this. changeDetector.detectChanges();
    }, 5000);
  }

In the other use case, the change detection only needed to happen when an Observable from a service was updated. That implementation used a pattern similar to this pseudo-code:

import { Component, ChangeDetectorRef, OnInit } from '@angular/core';

constructor(private stateMgmtService: StateMgmtService, private changeDetector: ChangeDetectorRef) {}

public messages$: Observable<MySpecialArray[]>
public list: Subscription:
public data: any[] = [];

ngOnInit() {
   this.changeDetector.detach(); 
   this.messages$ = this.stateMgmtService.getSomeData();
   this.list = this.message$.subscribe({
      next: x => {
         // . . . do some calculations against x
         this.data = x;
         // Only detect changes on next
         this.changeDetector.detectChanges();
      }
   })

}

And here’s the component.html:

<!-- https://material.angular.io/cdk/scrolling/overview -->
<cdk-virtual-scroll-viewport [itemSize]="8" class="point-viewport">
  <div *cdkVirtualFor="let location of data”>
    {{location}}
  </div>
</cdk-virtual-scroll-viewport>

Caveat Emptor. There are technical debt issues when you implement manual control on Angular change detection. Instead of a nice, loosely coupled approach to handling UI updates, you step into potentially creating inconsistencies between how different components handle updates and this adds complexity to your application. This can also affect how you write unit tests and can introduce unforeseen bugs. With all that said, sometimes you have to make decisions based on your unique requirements and you have to take the best approach for your circumstances.

Performance comparison between readAsDataUrl and createObjectURL

If you work with applications that handle uploading images as blobs then you’ve most likely wondered if it’s faster to convert the image using FileReader.readAsDataUrl() or URL.createObjectURL(). For our implementations in the geographic mapping industry we often typically request dozens, hundreds or sometimes thousands of relatively tiny map tile images such as .png and .jpeg in a single user session. There’s always a question of loading and rendering performance.

I was working on a related customer question and I was curious which one is faster in our use cases, so I did some simple testing. A common example of an online web map contains tiles that are 256×256 and vary in size from around 2Kb to 15kb. I assumed for the file types and sizes we use the results would be different because I’d read that createObjectUrl() is typically faster.

TLDR;

The results were surprising to me. For our use cases with relatively small .png images I saw the following results:

  • Chrome: readAsDataUrl() was consistently faster uncached
  • Firefox: createObjectUrl() was consistently faster uncached
  • Safari: was inconsistent between the two.

Test appHere is the link to the test app.

Note that Safari had unpredictable performance in that sometimes readAsDataUrl() was faster than createObjectUrl(). I saw the same behavior for cached and uncached tests and didn’t have time to investigate further.

YMMV!

Just a caveat that since we use lots of small images, your mileage may vary if you use larger images. I hope someone reads this and devises a test for larger images and then shares the results.

The Tests

I tested basic performance of pulling a map tile image from a CDN then using performance.now() to determine the time to create the image and then append it to an HTML list element. I did try to build the code in a way that each loop of the test used a different image to avoid any unintentional optimizations, such as sharing an image in-memory, or in-browser caching. I also ran the comparative tests recursively to try and normalize for readAsDataUrl() being asynchronous. I didn’t have time to investigate memory usage between the two patterns.

Note that testing with the browser console closed will be significantly faster than testing with the console open.  I used a 2018 Macbook Pro, 16GB with DDR4 RAM. I cleared the browser cache before each loop and I used this test app. In the code, each pattern goes through 25 loops and is averaged.

Chrome 80
Test results averaged
Test 1
(ms)
Test 2
(ms)
Test 3
(ms)
Test 4
(ms)
readAsDataUrl0.7890.8270.8130.839
createObjectURL1.6841.6381.6411.544
Firefox 73Test 1
(ms)
Test 2
(ms)
Test 3
(ms)
Test 4
(ms)
readAsDataUrl1.481.281.281.2
createObjectURL1.120.841.041.0
Safari 13Test 1
(ms)
Test 2
(ms)
Test 3
(ms)
Test 4
(ms)
readAsDataUrl0.360.280.720.68
createObjectURL1.960.60.561.6

Conclusions

If you are only uploading a few smaller images, then wondering which approach is faster probably isn’t a good use of your time – either one is good. If you handle hundreds or thousands of smaller images per user session then it might be worth some testing. Based on these quick tests, and more testing is needed to be truly definitive, it really depends on which browsers your users prefer. For example, if you are building hybrid apps then you have control over which browser. In a pure web application you don’t typical have control over what users use in the wild.

I didn’t test larger sized images or images of a different type, such as .jpeg. I’m curious what type of test results those might produce.

Injecting Custom Files into Ionic Build

This post is about including custom .html, .css and .js files in an Ionic 3.x+ project so that they get copied from /src to /www during the build process. Basically, we are talking about files that are handled separately from the webpack compiler process, but you want them to be swept into the test or production build when they get updated.

Step 1: Modify your projects main package.json file in the root directory to override the copying process in ionic:build and ionic:serve. In this example we are referencing a custom library called ionic-config-override.js that controls what we want to copy, and this library lives in the root directory, as well.

{
     . . .
     “scripts”: {
          . . .
          "ionic:build": "ionic-app-scripts build --copy ionic-config-override.js",
          “ionic:serve”: "ionic-app-scripts serve --copy ionic-config-override.js"
     }
     . . .
}

Step 2: Create a JavaScript file of the same name you referenced in package.json for example ionic-config-override.js. In this new JavaScript library use the node.js fs-extra file utility to copy the files you want when the build process is run. Here’s one example:

var fs = require(‘fs-extra’);
fs.copy(‘src/special.html’,’www/special.html’);

Step 3: To test locally instead of using ionic serve, run this new script using the command line command npm run ionic:serve. Or, if you are ready to test on a device run the script using npm run ionic:build android and then when that is completed successfully use ionic cordova run android.

References:
Ionic –copy command

The Copy command source code can be found in your project path @ionic/app-scripts/config/copy.config.js

Additional information can be found on the Ionic forums.