top of page

RxJs vs Signal memory footprint

Writer: ZsoltZsolt

Yet another Signal benefit.



In the last article I showed how it’s possible, but way harder to leak memory with Signals than Rx objects. Now let's see their normal consumption.


As you might already know, Observables/Subjects and Signals are not exactly meant for the same purpose. For one, Signals are a store of value, while Rx objects are more than perfectly suited for eventlike behavior. In many if not most cases they are interchangeable especially inside components.

Without further ado, let’s see the TL;DR; version, a picture instead of a hundred words, right?

WriteableSignal’s size is 1.5 times of BehaviorSubject’s

The setup is simple: an otherwise empty Component with an array holding 100k of each examined object (~table rows), checked in isolation, one by one. 


  • Shallow is the size of each object in the 100k one by one, in Bytes

  • Retained is all the memory that’d be GCd if the object was deleted, in Bytes

  • All retained is the retained size of the array holding all the 100k elements.

  • notes: Signals array’s retained size remained the same for the computed case, while for the combineLatest case the Subject’s array’s retained size shrunk to 0.48MB from 6.05MB

100k BehaviorSubjects
100k combineLatest Observables from 100k BehaviorSubjects

The code was as follows:

export class RxVSignalComponent {
  sigs: WritableSignal<number>[] = [];

  constructor() {
    for (let i = 0; i < 100000; i++) {
      this.sigs.push(signal(i)); // <- first row in table
    }
  }
}
// then tried 
const sig = signal(0);
for (let i = 0; i < 100000; i++) {
      this.compSigs.push(computed(() => sig() + 1); // <- second row in table
}

// then compSigs became this
  for(let i = 0; i < 100000; i++) {
    this.compSigs.push(computed(() => { // <- third row
        return this.sigs[i]() +( this.sigs[i+1]?.() ?? 0);
    }));
  }

// then with an empty component again tried the same things with Rx (same with subscription)
    for (let i = 0; i < 100000; i++) {
        this.subArr$.push(new BehaviorSubject(i)); // <- fourth row
    }

// and then with an Observable combination, similarly to computed
    for (let i = 0; i < 100000; i++) {
      const comb$ = combineLatest([
        this.subArr$[i],
        this.subArr$[i + 1] ?? of(0),
      ]).pipe(map(([a, b]) => a + b));
      this.combObsArr$.push(comb$); // <- last row 
    }

Is RxJs way more memory-efficient than Signals?

The retained memory size of Subjects gave me a hint. It’s not just pure Subjects, Subscriptions and Signals in an app, it’s the combination and all the indirect cost that comes from the framework using them. First of all Observables and Subjects are useless without Subscriptions, so for every few of them there’ll be one Subscription at least. That already levels the field a bit as Signals don’t need such.


But it all comes together when we actually use them. So I took the simplest case, 100k BehaviorSubjects vs 100k WriteableSignals and used them in a @for .

    @for(sig of sigs; track sig){
      {{ sig() }}
    }
    <!-- @for(subj$ of subArr$; track subj$){
        {{subj$ | async}}
    } -->

The full snapshot size of the application (with forced GC of course) in these two cases:

So after all in real applications, all the Rx overhead that’s added to that one Subject to work makes it significantly heavier.


If you liked the article please don’t forget to click and hold that clap button from 1–50 based on how much you enjoyed reading. Thanks!

Comments


SIGN UP AND STAY UPDATED!

Thanks for submitting!

© 2019 ZD Engineering. Proudly created with Wix.com

bottom of page