I've found a weird occurrence, when working with Ngrx Store and Angular. Basically the goal of my task is shown in this example with about 10k entries in the Person[]:
My goal is to show certain people with specific surnames. This is the first revision I came up with, which is very slow:
export const selectPerson = createSelector(
selectCitizensState,
(citizensState: CitizensState): Person[] => citizensState.people ?? []
);
export const selectPeopleWithSurname = createSelector(
selectPerson,
selectSelectedSurnames,
(people: Person[], surnames: string[]) => {
if (surnames.length === 0) {
return people;
}
return people.filter((person) => surnames.includes(person.surname));
}
);
I came up with this on accident and it's way faster then the code above:
export const selectPerson = createSelector(
selectCitizensState,
(citizensState: CitizensState): Person[] => [...(citizensState.people ?? [])]
);
Why is that so? I am just creating a shallow copy of the store property. I can imagine that it has something to do with Ngrx's change detection or something, but I am not sure.
Immutability and memoization are why parts seem slow/fast. (citizensState: CitizensState): Person[] => [...(citizensState.people ?? [])]
in your fast version is creating a new array instance every time the selector runs. NgRx selectors use memoization to cache and avoid unnecessary recomputation. A selector only re-runs when its inputs change.
In your slow version you are returning the original reference from the store. This causes selectPeopleWithSurname
to rely more heavily on checking contents, and possibly re-run more often than necessary.
If people is the same reference as before, NgRx assumes it's unchanged. But if the surnames change and you still return the same array reference, the second selector (selectPeopleWithSurname) must re-evaluate .filter() every time. That .filter()
call over 10k records is expensive. If you create a new array via [...people]
, NgRx treats it as a new input which can actually prevent unnecessary re-runs downstream if you're using OnPush change detection or a pipe like *ngFor
.
Check out this article about Memoization with Selectors in NgRx by Keerti Kotaru