@rx-angular/cdk/coalescing
A small typed and documented convenience helper to apply performance improvements over coalescing techniques.
@rx-angular/cdk/coalescing
is designed to help developers improve performance in high frequently changing values.
Besides a well documented and typed API it also provides a set of marble diagrams for better visual understanding as well as smart defaults.
Key features
- ✅ Coalescing techniques as RxJS operators
- ✅ Configurable durationSelector for all kinds of scheduling methods
- ✅ Scope coalescing to a specific component or object
- ✅ Typed methods
- ✅ IDE inline documentation
Demos:
Install
npm install --save @rx-angular/cdk
# or
yarn add @rx-angular/cdk
Documentation
Resources
Example applications: A demo application is available on GitHub.
Motivation
Coalescing means multiple things "merge" into one.
If two or more things coalesce, they come merge togeather into one thing or system. Natively Angular is using this under the hood already for a long time.
In RxAngular coalescing is used for merging multiple emissions, streams or calls that happen during a given timeframe. As one of the key building blocks for performance, this technique is utilized to improve the change detection cycles of angular applications.
The next example shows the effect of coalescing visualized in flame charts.
no coalescing vs. coalescing on microtask. visualized in flame charts
The non-coalesced component has three consecutive heavy computations in the template while the coalesced component only has to do the same computation once in order to complete the same job. Even on a small components scale the difference in performance can be significant.
Available approaches
There are 2 places in Angular we have coalescing already implemented in the framework:
- Coalescing of
ApplicationRef#tick
calls (re-rendering/re-evaluation of the app) triggered by e.g.ChangeDetectorRef#markForCheck
orɵmarkDirty
. - The flag
ngZoneEventCoalescing
inBootstrapOptions
- RxAngular adds another option where we can apply those techniques manually wherever we want.
The Benefits
- ✅ Coalescing techniques as RxJS operators
- ✅ Configurable durationSelector for all kind of scheduling methods
- ✅ Scope coalescing to a specific component or object
- ✅ Memory leak save through WeakMaps
- ✅ Typed methods
- ✅ IDE inline documentation
Before we dive into the usage of this package we may want to understand already existing coalescing mechanisms in Angular and why it is used to get better performance.
Coalescing of ApplicationRef#tick
calls
As chances are high multiple changes occur at the same time Angular's change detection would end up getting triggered also multiple times. This is the reason why Angular implemented coalescing for those calls.
In the following image we see 3 changes that call ChangeDetectorRef#markForCheck
.
The internal logic then delays these calls for a while by using requestAnimationFrame
and calls ApplicationRef#tick
only one time after the next animation frame arrives.
This way Angular's change detection and re-evaluation/re-rendering of the app get executed only once for all calls that fall into the duration from invocation until the next animation frame lands.
If we visualize the same behavior based on flame charts, we can understand the internal logic and naming of the different steps in that process more technically. The graphic shows the start of the coalescing duration, the different browser events and where the execution of certain logic is moved to.
With that information, we should be able to reflect this concept also onto other usages in Angular.
Coalescing with ngZoneEventCoalescing
settings
Angular's bootstrap method can be configured to use a config property called ngZoneEventCoalescing
.
platformBrowserDynamic()
.bootstrapModule(AppModule, { ngZoneEventCoalescing: true })
.catch((err) => console.error(err));
This setting applies the technique of coalescing to fired events bound by Angular.
It will coalesce any event emissions occurring during the duration of an animation frame and after that, run ApplicationRef#tick
only one time instead of multiple times.
This is mainly impactful if we deal with event-heavy templates. The diagrams below shows the difference between 2 events with and without coalescing.
As these situations typically occur across multiple components or are hard to scope and demo, we list some staged examples:
Multiple Events on the same element
<button (click)="doStuff()" (mousedown)="doStuff()">click</button>
Nested Events on multiple elements
<div (mousedown)="doStuff()">
<button (click)="doStuff()">click</button>
</div>
Event Bubbling of same event on multiple elements
<div (click)="doStuff()">
<button (click)="doStuff()">click</button>
</div>
RxAngular coalescing operators
While developing RxAngular, one of the first things we had to tackle for performant change detection was coalescing of ChangeDetectorRef#detectChanges
calls on component level,
but in fact, the shipped logic can be applied anywhere.
There are 2 main pieces to understand:
- the coalescing mechanism
- the scoping mechanism
In the section usage we will go into more detail.
Marble Diagram
Setup
The coalescing features can be used directly from the cdk
package or indirectly through the template
package.
To do so, install the cdk
package and, if needed, the packages depending on it:
- Install
@rx-angular/cdk
npm i @rx-angular/cdk
// or
yarn add @rx-angular/cdk
Usage
As coalescing was already explained in the Angular context, we can take that one step further and look at it in a more agnostic way.
Imagine the following code snippet which executes heavy computation:
function doStuff(value) {
console.log(value);
}
If we would call it multiple times we would cause 3 times the computation.
from([1, 2, 3]).subscribe(doStuff); // 3 x doStuff logs 1, 2, 3
RxAngular's coalesceWith
operator helps to merge together when applied to the source.
from([1, 2, 3]).pipe(coalesceWith()).subscribe(doStuff); // 1 x doStuff logs 3
Coalescing duration
By default, the duration in which values get united is derived from a micro task which executes immediately after the synchronous code got executed.
See the diagram for details:
To have more fine-grained control over the duration of coalescing an optional parameter durationSelector
is provided.
durationSelector
is of type Observable<unknown>
and the first emission of it terminates the coalescing duration.
You could pass e.g. interval(0)
as durationSelector
to use a setInterval
as duration period.
💡 Pro Tip Even a longer duration based on milliseconds, e.g.
interval(500)
can be used as duration.For more information on the different scheduling options you could have a look at the different scheduling API's like
queueMicroTask
,requestAnimationFrame
,setTimeout
,postMessage
orrequestIdleCallback
.
A real life example where coalesceWith
comes in handy is runnning manual change detection with ChangeDetectorRef#detectChanges()
.
The below diagram displays the cycle of updates, coalescing and rendering of values in a component.
Coalescing scope
If we think about the underlying principle of coalescing a little bit more we may ask our self how the logic knows what to do? How is it done that some work that is scheduled multiple times get executed only once? Surely there must be a variable stored somewhere that knows if coalescing is currently ongoing or not.
Let's make up a small example to understand the situation a little bit better.
In the following snippet we see the same logic from above but applied to 2 different subscriptions.
Both components schedule changes and use coalesceWith
inside.
from([1, 2, 3]).pipe(coalesceWith(queueMicroTask)).subscribe(detectChanges); // 1 x detectChanges renders 3
from([1, 2, 3]).pipe(coalesceWith(queueMicroTask)).subscribe(detectChanges); // 1 x detectChanges renders 3
The result is quite unintuitive. Both subscriptions trigger the detect changes call and the component got re-rendered 2 times.
Why is this the case?
If we recall the code snippet from above we see the number logged to console was 3
out of the series of 1
, 2
, 3
.
from([1, 2, 3]).pipe(coalesceWith()).subscribe(doStuff); // 1 x doStuff logs 3
It makes sense because we want to render only the last update to the component. To do this, coalesceWith
maintains a flag for each subscription.
The wanted behavior should execute change detection only once per component.
This can be achieved by scoping the flag that maintains coalescing to a specific thing. e.g. the component.
from([1, 2, 3])
.pipe(coalesceWith(queueMicroTask, this))
.subscribe(detectChanges); // 0 x detectChanges no render
from([1, 2, 3])
.pipe(coalesceWith(queueMicroTask, this))
.subscribe(detectChanges); // 1 x detectChanges renders 3
With this in mind, we can go one step further and look at change detection across multiple components.
The following diagram illustrates change detection in component level:
⚠ Notice: Be cautious with globally shared coalescing scopes. It could lead to unwanted behavior and loss of updates when used incorrectly.
Again, why is this the case?
As we have now only one scope for every scheduled work from multiple components we will end up updating only the last one. Same as in the first example where we only log the last emission.
from([1, 2, 3]).pipe(coalesceWith()).subscribe(doStuff); // 1 x doStuff logs 3
Just imagine the same for components:
from([component1, component2, component3])
.pipe(coalesceWith())
.subscribe((component) => component.cdr.detectChanges()); // only component 3 gets called
Example usage in RxAngular
As RxAngular/cdk packages are not only here to build other tools but also to build RxAngular it self lets see where we used it under the hood.
In the template package we have a couple of directives and pipes that use the coalescing logic internally. It is done in a way where the directives and services automatically take the most performant scope to bind coalescing to.
The example below shows multiple components rendering the same or parts of the same value. The scopes are applied automatically and named for all different usages.
The different usages are as follows:
- As operator in the component class
- As pipe in the component's template
- As structural directive in the component's template