3

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.

callstack

Is this the intented behavior?

  • We need more information. "scheduling the change detection all time" -- what does that mean? Change detection will run whenever a (Zone.js monkey-patched) asynchronous event occurs -- e.g., a keypress, a button click, an XHR response. By default, all template bindings in all components are checked each time change detection runs. Whatever component displays your large data array should be using the `OnPush` change detection strategy so it doesn't get checked all the time. Input properties that are arrays are compared by reference (`===`). – Mark Rajcok May 20 '16 at 19:08
  • It means for some reason the change detection is being performed periodically. Not as you said, in response of a keypress, XHR, mouseclick, etc. – Jairo Andres Velasco Romero May 20 '16 at 19:12
  • When a periodic timer runs inside Angulars zone (in your code or Hansontable) then change detection runs with each timer event as well. – Günter Zöchbauer May 20 '16 at 19:17
  • But why a deep comparison is made? a simpler reference change test is enough, right?. – Jairo Andres Velasco Romero May 20 '16 at 19:22
  • I realized that in my case BrowserSync is the trigger of the periodic change detection. But still my question is valid: is the deep value equivalence comparison the intented behavior? – Jairo Andres Velasco Romero May 20 '16 at 19:26
  • There is no deep value comparison done by Angulars change detection (as already mentuoned above) – Günter Zöchbauer May 20 '16 at 19:34

2 Answers2

4

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

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

  • Not sure what you mean by "equivalence testing" in your last sentence. In `devMode` the change detection does the same as in `prodMode` (value equality check for primitive types and reference equality check for arrays and objects). The only difference is that change detection is run twice to check if change detection itself didn't cause any change. – Günter Zöchbauer May 20 '16 at 20:26
  • 1
    In view_utils.ts, checkBinding(throwOnChange: boolean, oldValue: any, newValue: any): boolean is invoked with throwOnChange = true only in devMode ( because of an assertion made in ViewRef_.checkNoChanges() in view_ref.ts ) then devModeEqual() is performed. In production mode devModeEqual() is not invoked due to assertions are disabled. – Jairo Andres Velasco Romero May 20 '16 at 20:32
  • 1
    You are right. I haven't seen this mentioned earlier when `devMode` was discussed. Thanks a lot, I learned something new today! – Günter Zöchbauer May 20 '16 at 20:36
  • After looking into this in detail, I too wonder if this is the intended behavior. – Mark Rajcok May 21 '16 at 03:02
3

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 view_utils.ts 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 change_detection_util.ts, 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);
  }
}

So if a template binding contains something that is iterable – e.g., [arrayInputProperty]="parentArray"then in devMode change detection actually iterates through all of the (e.g. parentArray) items and compares them, 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 collection.ts 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 lang.ts (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 View*.detectChangesInternal() methods should be on the call stack.

Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492