Hi, In this article, I will demonstrate how signals can significantly improve the performance of your application and explain why you should consider using them.
You might already be familiar with how signals utilize ChangeDetection in Angular. Today, however, we will focus on RxJS, specifically one of my favorite operators: combineLatest. The concepts discussed here are also applicable to other operators.
I'm sure your Angular app has a lot of lines of code similar to this example
public combinedStream$ = combineLatest([first, second, third]).pipe(
...
);
We know that combineLatest triggers for the first time when all items—in this case, first, second, and third—have emitted their initial values. After that, it triggers every time any of the items in the array emit a new value.
Let's imagine that first, second, and third are asynchronous operations that emit new values at roughly the same time, causing the DOM to update (instead of DOM update I just console.log counter value).
What do you think will be in the console?
@Component({
selector: 'my-component',
standalone: true,
imports: [CommonModule],
template: ``,
})
export class MyComponent {
public first = interval(1000).pipe(take(3));
public second = interval(1000).pipe(take(3));
public third = interval(1000).pipe(take(3));
public counter = 0;
public combinedStream$ = combineLatest([this.first, this.second, this.third]).pipe(
take(10),
map(() => ++this.counter),
);
ngOnInit() {
this.combinedStream$.subscribe(console.log)
}
}
Output:
1
2
3
4
5
6
7
Why 7 and not 9 (take(3) x 3 streams = 9) because combineLatest triggers for the first time when all items—in this case, first, second, and third—have emitted their initial values.
Here is the problem all streams emitting the same time interval(1000)
// 1 second
1
// 2 second
2
3
4
// 3 second
5
6
7
Can we skip the 2, 3, 5, and 6 executions to improve our performance? Yes we can, but it's just a trick with debounceTime(0) and I a lot of Angular developers don't even think about such performance loss
@Component({
selector: 'my-component',
standalone: true,
imports: [CommonModule],
template: ``,
})
export class MyComponent {
public first = interval(1000).pipe(take(3));
public second = interval(1000).pipe(take(3));
public third = interval(1000).pipe(take(3));
public counter = 0;
public combinedStream$ = combineLatest([this.first, this.second, this.third]).pipe(
take(10),
debounceTime(0),
map(() => ++this.counter),
);
ngOnInit() {
this.combinedStream$.subscribe(console.log)
}
}
Output:
1
2
3
More than 2x performance right?
I have good news for you: Signal does this under the hood
@Component({
selector: 'my-component',
standalone: true,
imports: [CommonModule],
template: ``,
})
export class MyComponent {
first = signal(0);
second = signal(0);
third = signal(0);
counter = 0;
computedSignal = computed(() => {
this.counter++;
console.log(this.counter);
return `${this.first()} ${this.second()} ${this.third()}`;
});
ngOnInit() {
interval(1000).pipe(take(3)).subscribe((value) => {
this.first.set(value);
this.second.set(value);
this.third.set(value);
})
}
}
Output:
1
2
3
I hope you found this helpful or at least interesting. Feel free to experiment with the example to gain a better understanding
Top comments (0)