DEV Community

loading...
Cover image for NgRx Selector Performance
Angular

NgRx Selector Performance

Stephen Cooper
Senior Engineer @G-Research. Writing and speaking about Angular and web tech.
・4 min read

NgRx selectors promise performance gains via memoization. However, we must take care when defining our selectors otherwise we may fail to benefit from memoization! In fact we may inadvertently degrade the performance of our application.

NgRx Selectors

If you are not familiar with NgRx Selectors then check out this talk from Brandon Roberts on selectors or the docs. They are basically a way of extracting data from your Store.

Next let's see how easy it is to fall into this performance trap!

Counter Application

To demonstrate the performance trap we will use a counter app. You can experiment with the code in this StackBlitz which complements this post.

There are two counters and a text box. We display the current value of each counter and the total of all counters.

Counter App

Our state has the following interface.

export interface CounterState {
  counter1: number;
  counter2: number;
  name: string;
}

export interface BusyState {
  //lots of updates happen here!
}

export interface RootState {
  counter : CounterState;
  busyState: BusyState;
}
Enter fullscreen mode Exit fullscreen mode

Note that we have two feature slices, counter and busyState. busyState, as the name suggests, receives a lot of updates.

Calculating the Total

As we do not want to store derived state in our store, we will need to calculate the total on the fly. There are a few ways to calculate the total to be displayed in our template. Each has its own performance characteristics which we will now examine.

Adding two numbers is a trivial operation but for the sake of this post let's imagine it is a very expensive computation which we must minimise calculating.

Calculate Total in the Component

We can calculate the total directly in our component using the injected store and the select operator.

// Component
constructor(private store: Store<RootState>){}

this.total$ = store.pipe(select(state => 
                             state.counter.counter1 + state.counter.counter2)
                        );
Enter fullscreen mode Exit fullscreen mode

However, with this approach the calculation will be re-run for every change to our state. That includes every change made to BusyState which are totally unrelated and will never change the value of the total! This is really bad for our performance so let's see if we can do better.

Calculate Total in Reducer with a Selector

As you may have guessed we are going to use selectors to improve the performance. We do this by using the creator functions, as described by Tim Deschryver, from @ngrx/store. Using these creator functions we can move the total calculation out of our component and into our reducer.

// Reducer
import { createSelector, createFeatureSelector } from "@ngrx/store";

const featureSelector = createFeatureSelector<CounterState>("counter");

export const getTotal = createSelector(
  featureSelector, s => s.counter1 + s.counter2
);
Enter fullscreen mode Exit fullscreen mode

We take as input our feature slice and return counter1 + counter2 to give us an observable stream of the total. We then use this in our component to display the total.

// Component
this.total$ = store.pipe(select(getTotal));
Enter fullscreen mode Exit fullscreen mode

Using this selector means that our total calculation is only run on changes to the counter feature slice. This is a great improvement as no longer is it being re-run for unrelated changes to BusyState. But let's not stop there we can do even better!

Understanding Memoization

At this point it is important to understand how the memoization of selectors work as we are still not taking full advantage of it.

Lets go back to the docs for selectors.

When using the createSelector and createFeatureSelector functions @ngrx /store keeps track of the latest arguments in which your selector function was invoked. Because selectors are pure functions, the last result can be returned when the arguments match without re-invoking your selector function. This can provide performance benefits, particularly with selectors that perform expensive computation. This practice is known as memoization.

The important part here is that @ngrx/store keeps track of the latest input arguments. In our case this is the entire counter feature slice.

export const getTotal = createSelector(
  featureSelector, s => s.counter1 + s.counter2
);
Enter fullscreen mode Exit fullscreen mode

To see why we can do better let's start updating counter.name via our text input. On every stroke an action is dispatched to update the name. On each update our total is being recalculated because it is part of the same feature slice.

Calculate with Composed Selectors

Using what we learnt from the docs we will re-write our getTotal selector to ensure that it is executed only when its own arguments change. We do this by composing it of a getCounter1 selector and a getCounter2 selector. These counter selectors will only emit new values when the specific counter updates. This in turn means that the arguments to our getTotal selector only change when the value of one of the counter changes.

// Reducer
export const getCounter1 = createSelector(
  featureSelector, s => s.counter1
);

export const getCounter2 = createSelector(
  featureSelector, s => s.counter2
);

// Composed selector
export const getTotal = createSelector(
  getCounter1, getCounter2, (c1, c2) => c1 + c2
);
Enter fullscreen mode Exit fullscreen mode

With this setup changes to the counter.name no longer cause the total to be recalculated! We finally are making full use of memoization and have ensured we only run the total calculation when we absolutely have to. This is the power of selector composition.

Real life scenario

While our demo app is too small to have performance issues, these principles can be applied with great effect to large applications.

In one app that I worked on we had a number of interdependent dropdowns, i.e updating the selection in one would filter the available options in the others. This was driven by selectors all working off the root store. I was tasked with investigating the sluggishness of these selectors. The first thing I did was start logging out every time each selector ran. It was hundreds of times!!

This is when I discovered the importance of composing your selectors. Making the changes, as outlined above, brought the number of selector calls down from hundreds to just a handful. The performance improvement was dramatic and the selectors were no longer sluggish.

Final Thoughts

If you are doing anything computationally expensive in your selectors then you want to ensure that you only run that code when you absolutely have to. Composing your selectors is one technique that enables you to achieve this and protect the performance of your application.

Discussion (7)

Collapse
maxime1992 profile image
Maxime

How do you deal with this when you receive an array of values? And how do you deal with it when you remap an array to another array? The reference will change even if the array contains the same references. Therefore any selectors based on one like that will be fired again.

Also, while this post is nice and the idea is good, I'd point out that it's probably not worth doing that for very simple like adding 2 numbers. This is stupid fast in the first place and anything that's not an object will give a value that can be cached/memoized for the next selectors. If you're doing heavy computation in a selector though, that'd be a good use case.

Collapse
scooperdev profile image
Stephen Cooper Author

I am not 100% clear which function you are referring to that is receiving the array? If its the selector then yes if it results in a new array or a different reference then it will fire a new value. I would argue that if the array is changing then it should be recalculated. If its within our control then we should check that we are not updating the array references when we know its contents will not be changing.

If that is outside of our control then we would have to decide what is more performant. To apply a custom distinctUntilChanged to the selector or just run the calculation again.

And yes I totally agree this is overkill for adding 2 numbers! I tried to make that clear in the post and that this technique is only required when you are running expensive computations based off your state.

Collapse
tieppt profile image
Tiep Phan • Edited
this.total$ = store.pipe(select(getRawTotal));

should be:

this.total$ = store.pipe(select(getTotal));
Collapse
scooperdev profile image
Stephen Cooper Author

Thanks, have updated. The danger of refactoring in a post!

Collapse
tieppt profile image
Tiep Phan

You're welcome! Btw, thank you for a nice post.

Collapse
danmt profile image
Daniel Marin

Awesome content. I love that technique, once I realized the performance improvements you can get out of it, I just couldnt stop using it.

Collapse
anduser96 profile image
Andrei Gatej

Great article 👍🏼
Thank you!