Angular2 detect changes using value equivalence or reference equality?


I am using Angular2-RC.1 and I have seen a poor performance when I setup a component having large data. I have a tabular component (wrapping Handsontable) and I expose a bindable Input property called "data". This property is usually bound to a large array (around one hundred thousand rows).

When I set my large dataset the change detection is causing a test of value equivalence over the whole array in the host component (not the owner of the input property).

@Component({ selector: "ha-spreadsheet", template: "<hot-table [data]="data"></hot-table>", directives: [ HotTable ], encapsulation: ViewEncapsulation.Emulated }) export class Spreadsheet implements OnActivate { data: { rows: Array<Array<number>> }; load(service) { this.data = service.getLargeDataSet(); } }

Here I show a callstack showing that the change detection is launched over the whole data. (the bold method is the runtime auto-generated change detection function for my host component) instead to simply compare the references.

<a href="https://i.stack.imgur.com/Kvagp.png" rel="nofollow"><img alt="callstack" class="b-lazy" data-src="https://i.stack.imgur.com/Kvagp.png" data-original="https://i.stack.imgur.com/Kvagp.png" src="https://etrip.eimg.top/images/2019/05/07/timg.gif" /></a>

Is this the intented behavior?


I have found the answer by myself. The standalone change detection process is comparing references (this is its behavior by design).

BUT when <strong>Production mode</strong> is NOT enabled then additional assertions perform equivalence testing over your component's data.


Although @Jairo already answered the question, I want to document in more detail the code flow that he mentioned in a comment on his answer (so I don't have to dig through the source code again to find this):

During change detection, this code from <a href="https://github.com/angular/angular/blob/master/modules/@angular/core/src/linker/view_utils.ts" rel="nofollow">view_utils.ts</a> executes:

export function checkBinding(throwOnChange: boolean, oldValue: any, newValue: any): boolean { if (throwOnChange) { // <<------- this is set to true in devMode if (!devModeEqual(oldValue, newValue)) { throw new ExpressionChangedAfterItHasBeenCheckedException(oldValue, newValue, null); } return false; } else { return !looseIdentical(oldValue, newValue); // <<--- so this runs in prodMode } }

From <a href="https://github.com/angular/angular/blob/master/modules/@angular/core/src/change_detection/change_detection_util.ts" rel="nofollow">change_detection_util.ts</a>, here is the method that only runs in devMode:

export function devModeEqual(a: any, b: any): boolean { if (isListLikeIterable(a) && isListLikeIterable(b)) { return areIterablesEqual(a, b, devModeEqual); // <<--- iterates over all items in a and b! } else if (!isListLikeIterable(a) && !isPrimitive(a) && !isListLikeIterable(b) && !isPrimitive(b)) { return true; } else { return looseIdentical(a, b); } }

<strong>So if a template binding contains something that is iterable</strong> – e.g., [arrayInputProperty]="parentArray" – <strong>then in devMode change detection actually iterates through all of the</strong> (e.g. parentArray) <strong>items and compares them,</strong> even if there isn't an NgFor loop or something else that creates a template binding to each element. This very different from the looseIdentical() check that is performed in prodMode. For very large iterables, this could have a significant performance impact, as in the OP scenario.

areIterablesEqual() is in <a href="https://github.com/angular/angular/blob/master/modules/%40angular/facade/src/collection.ts" rel="nofollow">collection.ts</a> and it simply iterates over the iterables and compares each item. (Since there is nothing interesting going on, I did not include the code here.)

From <a href="https://github.com/angular/angular/blob/master/modules/@angular/facade/src/lang.ts" rel="nofollow">lang.ts</a> (this is what I think most of us thought change detection always and only did -- in devMode or prodMode):

export function looseIdentical(a, b): boolean { return a === b || typeof a === "number" && typeof b === "number" && isNaN(a) && isNaN(b); }

Thanks @Jairo for digging into to this.

Note to self: to easily find the auto-generated change detection object that Angular creates for a component, put {{aMethod()}} in the template and set a breakpoint inside the aMethod() method. When the breakpoint triggers, the <em>View</em>*.detectChangesInternal() methods should be on the call stack.