Today I want to share one important thing about usage of RxJS streams and async pipe in Angular applications. I still face some misunderstanding here or even the facts that people don’t suspect any potential issues with the code.
It’ll be very boring and probably not very useful if I just show code sample with unexpected behavior and code sample with expected behavior, because it might look like “Why are you doing it at all? Isn’t the code looks weird?”. So yeah, I’ll start from the preconditions and use case to explain what I wanted to achieve and how I wanted to achieve it.
- On the project I’m using Angular as a main framework (as you can guess from the title) and ngrx as a primary data management tool;
- 95% of the components I have use ChangeDetectionStrategy.OnPush to avoid any redundant change detection cycles. As a result I use a lot of async pipes and RxJS streams directly from ngrx Store;
- In the project I have list of filters which user can apply to narrow the search scope. These filters are just a plain list however we want to group them by some criteria and show as a expand/collapse tree to improve navigation and UX;
As a result I wanted to implement a solution, where filters are stored in the ngrx Store, because in all other parts of application it’s much more convenient to use these filters as a plain list to be able to quickly filter them out.
On the other hand component which is responsible for rendering filters as an expand/collapse tree will receive the stream from the ngrx Store, apply the transformation to the stream’s values and recursively render all the branches of the resulting tree.
Now we are finally ready to see some code. The code snippet looks quite big, but I’ve tried to make it as small as possible to remove all noise and leave only the problem itself.
When I’ve finished up with the code I noticed that the performance of this component is quite low (number of items in the production’s filters list was around 10 thousands). I was surprised, because rendering even 10 thousands of items should not be a problem (in this article I will not discuss the fact, that there are ways to avoid rendering huge DOM trees, for example, to render only visible parts of huge lists).
I’ve started to investigate the problem and found that actually the most of CPU time was spend inside map function, the function which is responsible for the initial list-to-tree transformation. This function was called around 100.000 times. In fact my expectation was that it will be called only once. What a surprise! If you run the sample application you’ll notice the same problem
To fix the problem at first we need to understand what just happened. And the answer in that particular case is quite simple: each time you create a subscription your subscription asks the stream for the last saved value (please, take into account, that not all streams will emit the last value for each new subscription, but in this sample I’m replicating the behavior of ngrx Store). This value emitted by the stream itself, it means that it should bypass all the transformations again, including our “lightweight operation”.
In Angular we create a subscription to an observable each time we use async pipe. So to fix this problem we will perform three actions:
- Reduce the number of async pipes where it’s possible to reduce number of subscriptions;
- Change ChangeDetectionStrategy from Default to OnPush to reduce number change detection cycles (actually the second bunch of calls is triggered by redundant change detection cycle);
- Cache result of our transformation for further use by using publishReplay operator;
Here is the resulting code (I’ve marked lines with changes with comments):
Also you can find the original source code on the GitHub (without fixes) and play with it: https://github.com/anton-gorbikov/angular-samples/tree/master/rxjs
Work with streams and reactive programming contains a lot of unexpected behaviors and potential problems. I encourage you to dive really deep into reactive programming because despite the fact that it contains some surprises in the long term it’s very beneficial and proper usage can lead to more clean code and design.
If you still have questions feel free to contact me in the comments below.
Also if you find this material useful don’t forget to subscribe and share it with your colleagues! Thanks!