Como ya sabrás, los Observables/Sujetos y las signals no tienen exactamente el mismo propósito. Por un lado, las signals son un almacén de valor, mientras que los objetos Rx son más que adecuados para el comportamiento de eventos. En muchos casos, si no en la mayoría, son intercambiables, especialmente dentro de los componentes.

Sin más preámbulos, veamos la versión TL;DR;, una imagen en lugar de cien palabras, ¿verdad?

El tamaño de WriteableSignal es 1,5 veces el de BehaviorSubject.

La configuración es sencilla: un Componente vacío con un array que contiene 100k de cada objeto examinado (~filas de tabla), comprobados de forma aislada, uno a uno.

Shallow: Es el tamaño de cada objeto en el 100k uno por uno, en Bytes
Retenido es toda la memoria que sería GCd si el objeto fuera borrado, en Bytes
Todo retenido es el tamaño retenido del array que contiene todos los elementos de 100k.
Notas: El tamaño retenido de la matriz de signals se mantuvo igual para el caso computado, mientras que para el caso combineLatest el tamaño retenido de la matriz de Sujetos se redujo de 6.05MB a 0.48MB.

CPU
1 vCPU
MEMORIA
1 GB
ALMACENAMIENTO
10 GB
TRANSFERENCIA
1 TB
PRECIO
$ 4 mes
Para obtener el servidor GRATIS debes de escribir el cupon "LEIFER"
100k BehaviorSubjects
100k combineLatest Observables from 100k BehaviorSubjects

El código era el siguiente:

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 
    }

¿Es RxJs mucho más eficiente en memoria que signals?


El tamaño de la memoria retenida de Subjects me dio una pista. No se trata sólo de Subjects, Subscriptions y Signals puros en una aplicación, sino de la combinación y de todos los costes indirectos que se derivan de que el framework los utilice.

En primer lugar, los Observables y los Subjects son inútiles sin Subscriptions, así que por cada pocos de ellos habrá al menos un Subscription. Eso ya nivela un poco el campo, ya que las Signals no las necesitan.

Pero todo cobra sentido cuando realmente las utilizamos. Así que tomé el caso más simple, 100k BehaviorSubjects vs 100k WriteableSignals y los usé en un @for .

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

El tamaño completo de la instantánea de la aplicación (con GC forzada, por supuesto) en estos dos casos:

Así que, después de todo, en aplicaciones reales, toda la sobrecarga de Rx que se añade a ese Asunto para trabajar lo hace significativamente más pesado.

Fuente