Does ngFor directive re-render whole array on every mutation?
Asked Answered
D

3

20

Let's say we have an array of items:

items = [
    { title: 'item 1'},
    { title: 'item 2'},
    /* ... */
];

And there is a template that renders this array:

<ul>
    <li *ngFor="let item of items">{{item.title}}</li>
</ul>

Wll angular2 rerender the whole array if I add/remove items via push/splice or will it only add/remove the markup for the corresponding items? If it does updates only, then is there any difference in mutation stategies -- should I prefer push/splice over array replacing? In other words, are these two approaches equivalent in term of rendering performance:

/* 1: mutation */
this.items.push({ title: 'New Item' });

/* 2: replacement */
var newArray = this.items.slice();
newArray.push({ title: 'New Item' });

this.items = newArray;
Duty answered 15/2, 2017 at 8:56 Comment(0)
W
23

No , it re-renders only when the array itself is replaced by a different array instance.

update

Thanks to Olivier Boissé (see comments)

Even when a different array instance is passed, Angular recognizes if it contains the same item instances and doesn't rerender even then.

See also this StackBlitz example

If the used IterableDiffer recognizes and addition or removal at the beginning or in the middle, then an item is inserted/removed at that place without re-rendering all other items.

The animations demonstrated in Plunkers in answers of this question How can I animate *ngFor in angular 2? also demonstrate that. In fact this kind of animation was a driving factor to get this implemented this way (in addition to general optimization)

Whited answered 15/2, 2017 at 8:57 Comment(11)
Angular doesn't re-renders the elements that were present in the previous array instance.Frechette
@OlivierBoissé that's what my answer tries to say. When the array instance is replaced by a different one with the same content, I'm pretty sure it will completely re-render, otherwise, as you said, it will render only added items and keep the DOM elements for the ones that were in the array previously.Coco
event with a new instance it's not completely re-render, I tested it by exloring the DOM, I have a new array and the items are not re-rendered because they were alredy there in the previous oneFrechette
@OlivierBoissé you don't have by any chance a stackblitz.com example to reproduce?Coco
https://stackblitz.com/edit/angular-dncqm2, inspect the first two <p> you will see they are not re-renderFrechette
@OlivierBoissé thanks a lot. Seems IterableDiffer was improved even more since I last tried. I added some CSS animation to the example you provided to make it more obvious.Coco
I think Angular compares with the === operator, because if I replace the existing ones by new objects (with the same properties/values), the items are re-renderedFrechette
For the items definitely object identity is used. Angular change detection never cares about object content (except when directly bound to a property). *ngFor doesn't depend on change detection alone, because it uses the IterableDiffer to find changes inside an object (array), but it also only checks object identity of the items in the array, not their properties or values.Coco
Does this mean you do not need to bother about using 'trackBy' to explicitly tell angular which ones to re-render?Dogear
If object identity is enough to uniquely identify all items in the collection, then you don't need trackBy, otherwise you can tell Angular that it should for example use the items id property to identify them, for example to apply add/remove/move animations. If you replace an item in the collection with a new instance which should be treated as the same entity, then use trackBy for Angular to be able to recognize it as the same (by the same id property value for example)Coco
It only works in the example because it's a shallow copy. It depends on how you're editing the list. Using the spread operator and adding an additional element, or even filtering out one element when using filter can prevent a rerender because the new array is a shallow copy. Using reverse will cause all but one element to rerender! If you reassign the list to be the exact same list of elements, by hardcoding explicitly, everything would rerender without using an effective trackBy function.Clothing
K
25

In addition to Gunter's answer, if you want to know which part of your UI is rendered/re-rendered you can with Chrome (even independent from any lib/framework) :

  • Open your debug panel
  • Menu (of debug panel) / More tools / Rendering

You should then see the following panel :

enter image description here

Toggle the Paint Flashing option on, and have some fun with your list.
If an area is flashing green, it has been painted / re-painted 👍.

EX : If you take the Plunkr in Gunter's answer : http://plnkr.co/edit/oNm5d4KwUjLpmmu4IM2K?p=preview and toggle the Paint Flashing on, add an item to the list and you'll see that previous items do not flash. (which means there's no repaint).

Keeling answered 15/2, 2017 at 10:35 Comment(0)
W
23

No , it re-renders only when the array itself is replaced by a different array instance.

update

Thanks to Olivier Boissé (see comments)

Even when a different array instance is passed, Angular recognizes if it contains the same item instances and doesn't rerender even then.

See also this StackBlitz example

If the used IterableDiffer recognizes and addition or removal at the beginning or in the middle, then an item is inserted/removed at that place without re-rendering all other items.

The animations demonstrated in Plunkers in answers of this question How can I animate *ngFor in angular 2? also demonstrate that. In fact this kind of animation was a driving factor to get this implemented this way (in addition to general optimization)

Whited answered 15/2, 2017 at 8:57 Comment(11)
Angular doesn't re-renders the elements that were present in the previous array instance.Frechette
@OlivierBoissé that's what my answer tries to say. When the array instance is replaced by a different one with the same content, I'm pretty sure it will completely re-render, otherwise, as you said, it will render only added items and keep the DOM elements for the ones that were in the array previously.Coco
event with a new instance it's not completely re-render, I tested it by exloring the DOM, I have a new array and the items are not re-rendered because they were alredy there in the previous oneFrechette
@OlivierBoissé you don't have by any chance a stackblitz.com example to reproduce?Coco
https://stackblitz.com/edit/angular-dncqm2, inspect the first two <p> you will see they are not re-renderFrechette
@OlivierBoissé thanks a lot. Seems IterableDiffer was improved even more since I last tried. I added some CSS animation to the example you provided to make it more obvious.Coco
I think Angular compares with the === operator, because if I replace the existing ones by new objects (with the same properties/values), the items are re-renderedFrechette
For the items definitely object identity is used. Angular change detection never cares about object content (except when directly bound to a property). *ngFor doesn't depend on change detection alone, because it uses the IterableDiffer to find changes inside an object (array), but it also only checks object identity of the items in the array, not their properties or values.Coco
Does this mean you do not need to bother about using 'trackBy' to explicitly tell angular which ones to re-render?Dogear
If object identity is enough to uniquely identify all items in the collection, then you don't need trackBy, otherwise you can tell Angular that it should for example use the items id property to identify them, for example to apply add/remove/move animations. If you replace an item in the collection with a new instance which should be treated as the same entity, then use trackBy for Angular to be able to recognize it as the same (by the same id property value for example)Coco
It only works in the example because it's a shallow copy. It depends on how you're editing the list. Using the spread operator and adding an additional element, or even filtering out one element when using filter can prevent a rerender because the new array is a shallow copy. Using reverse will cause all but one element to rerender! If you reassign the list to be the exact same list of elements, by hardcoding explicitly, everything would rerender without using an effective trackBy function.Clothing
F
0

In Angular 10 my whole list of elements was rerendering even when previous items were untouched. I used trackBy in my ngFor and it fixed the problem.

You can use it like this <li *ngFor="let chatMessage of chatMessages; trackBy: trackChatMessage">

And then in your .ts file add function trackChatMessage that returns unique value string.

trackChatMessage(index: number, chatMessage: ChatMessage) {
return chatMessage.id;
}
Fermentative answered 26/7, 2023 at 12:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.