3

I am a part of an enterprise application in Angular 11. We use the NgRx store heavily and I was wondering if we can improve our application memory footprint by optimizing the subscriptions (currently the memory footprint is around ~200-220 MB and live JS Heap size ~76-85 MB).

To us, performance optimization is really important, as our clients don't refresh or close the browser window throughout the day and since we have enough subscriptions to serve our clients I need help to keep the memory footprint below the accepted limits and to not impact our clients.

We use subscriptions in the following manner:

private destroyed$: ReplaySubject <boolean> = new ReplaySubject(1);


ngOnInit(): void {
    this.accountProgress$ = this.store.pipe(select(appState => appState.account.accountProgress), takeUntil(this.destroyed$));
    ...
    ...
    this.accountProgress$.subscribe(() => {...})
}

ngOnDestroy(): void {
    this.destroyed$.next(true);
    this.destroyed$.complete();
}

Likewise, we have many other observable subscriptions that listen to different parts of App State. I have few queries:

  • Can the subscription this.accountProgress$.subscribe(() => {...}) still cause a memory leak? If it is used multiple times within the same component to fetch data in different methods? Should we use takeUntil() or take(1) with this kind of subscriptions as well (we have this at few places but I am not sure if it helps)? Or make use of share operators such as publish() + refCount() or share({refCount: true}) with this.store.pipe(select(appState => appState.account.accountProgress))?
  • Sometimes we need to dispatch an action based on the value we receive within the subscription, I know that this is an anti-pattern that we should not dispatch an action within a subscription, but what can be done, as few stores depend on API response and that is quite uncertain when it will return the data which will decide the further execution of the business logic
  • Are there any other ways to optimize? (Please note that we follow the aforementioned syntax of takeUntil() everywhere in our components wherever required, are there any other ways to prevent the memory consumption when dealing with NgRx store, Effects and Observables)
patrick.1729
  • 4,222
  • 2
  • 20
  • 29
  • 1
    takeUntil is fine, in case you are trying to get rid of memory leaks. the problem is in something else. dispatching action inside of a subscription is probably better to move to effects. it shouldn't affect the performance anyhow, but at least it would look cleaner – Andrei Feb 06 '21 at 11:14
  • Something I have noticed is that the "memory leak" in our application increases if the Redux dev tools extension is enabled. Try disabling it and see what the performance is like with it disabled. – AliF50 Feb 06 '21 at 18:44
  • @AliF50 the shared stats are on a normal system without any redux extension :) – patrick.1729 Feb 06 '21 at 18:58

2 Answers2

2

Can the subscription this.accountProgress$.subscribe(() => {...}) still cause a memory leak?

A while ago, I wrote here an answer that describes why memory leaks occur when using a Subject in RxJS. So, in this case I'd say it won't, because you're using takeUntil(this.destroyed$).

If it is used multiple times within the same component to fetch data in different methods?

If takeUntil is still there, there won't be any memory leaks. But, there is an important aspect that you should be aware of.
Suppose you have a subject which has a few subscribers:

const s = new Subject();

const o1$ = s.pipe(
  a(),
  b(),
  c(),
  d(),
);

const o2$ = s.pipe(
  a(),
  b(),
);

If you subscribe to o1$ 3 times, there will be 3 distinct instances of each operator used until s.pipe, namely a, b, c, d. This might be something you'd want to avoid and you can do so by using share(), shareReplay() operators:

const o1$ = s.pipe(
  a(),
  b(),
  c(),
  d(),
  share(),
);

// now, when `o1$` is subscribed, each of the `a-d` operators will have exactly one instance
o1$.subscribe()
o1$.subscribe()
o1$.subscribe()

Should we use takeUntil() or take(1) with this kind of subscriptions as well

It depends, take(n) will complete after the n-th values has been emitted and takeUntil(notifier$) will complete when notifier$ emits.

Sometimes we need to dispatch an action based on the value we receive within the subscription ...

Maybe instead of doing this in the subscription, you can use the tap operator, which is well-known for side effects.

data$ = this.store.select(yourSelector).pipe(
  /* ... */
  tap(result => this.store.dispatch(anAction))
  /* ... */
)

Also, the data$ observable from above can be used in conjunction with the async pipe, so you don't have to deal with manual subscription/unsubscription.

Andrei Gătej
  • 11,116
  • 1
  • 14
  • 31
1

Your actual methods to handle subscriptions are great, but I think the best way to handle rxjs code in Angular is to use the async pipe and good data composition.

You have to think in all the data needed to render your component and combine it to get just one final observable to be subscribed in HTML, in that way you can take advantage of ChangeDetectionStrategy and trackBy for *ngFor with reactive data.

Follow these links to get a better undertanding:

Hope I helped you a little.

patrick.1729
  • 4,222
  • 2
  • 20
  • 29