As this is a series here the links to all parts:
đź“ť New possibilities with Angular's push pipe - Part 1
đź“ť New possibilities with Angular's push pipe - Part 2

Working implementation under Rx-Angular :
-  npm i @rx-angular/template -S
Live Demo:
-  stackblitz.com/rx-angular

Angular's change detection is tied to a library called zone.js.

The main idea behind the usage of zone.js is to detect asynchronous changes in your application and call the rendered function to update the related HTML in Angular.

For this convenience we pay a big price.

Overperforming dirtyChecking and render calls, manual patching, and hacks for third party libs are just the tip of the iceberg. In heavily dynamic user interfaces the performance drawbacks introduced through zone.js could become a show stopper.

The push pipe is the first of a whole set of tools under @rx-angular/template to enable fully reactive applications as well as zone-less performance in zone-full applications.

One of the tools is the push pipe, the main focus on this series.

We will go into full detail later but nevertheless I want to give some rough overview of its benefits.

By default the push pipe renders Angular’s component tree in a very different way than Angular would do it natively.

Angular when detecting a change marks the component tree dirty up to the root component, and then rerenders the full path until the component that caused the change, and all its child components related to the change.



The push pipe renders locally. It triggers change detection and rendering only in the very component where the change got introduced, and the child components effected by the change.

The important part here, this is implemented in a scalable and efficient way, where we dont trigger rendering unnecessary.

Lets see the tricks and details of this great performance tool by understanding the native implementations of Angular first.

TL;DR


This article is rather for technical understanding than a getting started guide. Please have that in mind when reading through or get yourself familiar with topics like Observables, Change Detection, Rendering and zone.js.

The main idea behind the push pipe is a new way of handling change detection locally instead of the global handling used in async pipe by Angular natively. It is implemented in a way we can get zone-less performance in zone-full applications.

Historically we started with Angular 2, showed some POCs and a rough set of problems that we have to solve on the way.

Below the implementation details of the async pipe get explained.

While understanding the original code base step by step we refactor it to a reactive style. This shrinked the codebase to less than a third as well as brought a more performance, but most importantly we got a flexibility and extensible codebase to work with.

In the next article we see how to tackle problems that occur when working with locality in mind, and later implement the push pipe in a simple version.

Push Pipes History

With the implementation of Angular’s new change detection, and the option to also detect changes manually over ChangeDetectorRef#detectChanges, a couple of new ideas started to get thought back in 2015 when Angular switched from watchers system of AngularJS to the new change detection mechanism.

(This is more than 5 years ago)

The door-opener for those ideas was the option to manually detect local changes with ChangeDetectorRef#detectChanges.

This was the fundamental implementation for mechanisms to avoid change detection run for the whole application components tree, over rendering of parent components and efficient rendering for apps at scale.

Some of the people that were doing the initial pioneer work were Victor Savkin and Rob Wormald. Rob especially did a couple of talks on that topic and sketched the first implementations of something he called "push pipe". A version  of the async pipe with locality in mind.

A good talk on the new change detection was done by Victor at ng-conf 2015, Change Detection Reinvented - Victor Savkin. Others followed and a very recent one given by Rob was done at ng-up What's New and Coming in Angular - Rob Wormald  where he also mentioned the idea of the async pipe and new ways of local change detection.

Over the years other people also invested in that idea.
Most with the same outcome.

Yes, incredible fast, but... It does not scale... It's is super hard to implement it in a stable way...

One recent demonstrations was done by Sander Elias at ngVikings conf 2018. Take a pause of reading and listen to his conclusions back then.

Implementing a new version of change detection independent of zone.js need lots of thinking to really have it working in a scalable and performant way.

One of the first problems with current change detection got mentioned by Victor already back than in 2015:

Changing a leafe component causes re-rendering/dirtyChecking of the full affected path until the root.


Another main problem was ChangeDetectorRef#detectChanges being unnecessary and it would cause over rendering/dirtyChecking of components easily and pretty early also in small code bases if not caught in the right way. Another critical problem was to group change detection runs together through pipes, directives and the component class to manage them in a unified way.

We will fix that problem and also understand how to implement it. :)
If you can't wait check out rxAngular and give it a try.

The async pipe implementation

From the previous section we read roughly about the obstacles we have to overcome for local change detection.

First, let's understand how the async is implemented to get a better understanding of our goal.

If we focus on the important lines we have an interface, subscription logic and a passed observable:

// https://github.com/angular/angular/blob/master/packages/common/src/pipes/async_pipe.ts#L70-L71
@Pipe({name: 'async', pure: false})
  export class AsyncPipe implements OnDestroy, PipeTransform {
  // ... Hidden for the sake of brevity
}

The interface PipeTransform enforces the implementation of the transform method:

export interface PipeTransform {
  transform(value: any, ...args: any[]): any;
}

As the pipe is marked impure, configured over the @Pipe decorator, the execution of the implemented transform function is triggered every time change detection is triggered. After that it's returned value will be rendered in the template.

Note, the implementation will also trigger change detection if the same value gets passed.

transform(obj: Observable<any>|Promise<any>|null|undefined): any {
  // ... Hidden for the sake of brevity
  // https://github.com/angular/angular/blob/master/packages/common/src/pipes/async_pipe.ts#L94
  this._subscribe(obj);
  // ... Hidden for the sake of brevity
  // https://github.com/angular/angular/blob/master/packages/common/src/pipes/async_pipe.ts#L97
  return this._latestValue;
}

Let's also take a look at the implementation of _subscribe:

// https://github.com/angular/angular/blob/master/packages/common/src/pipes/async_pipe.ts#L113-L118
private _subscribe(obj: Observable<any>|Promise<any>|EventEmitter<any>): void {
  this._obj = obj;
  this._strategy = this._selectStrategy(obj);
  this._subscription = this._strategy.createSubscription(
  obj, (value: Object) => this._updateLatestValue(obj, value));
}

_subscribe takes either a Promise or an Observable. Internally it uses the propper strategy for the passed value.

In the strategies callback it calls _updateLatestValue. There _latestValue gets the latest value assigned.

// https://github.com/angular/angular/blob/master/packages/common/src/pipes/async_pipe.ts#L140-L145
private _updateLatestValue(async: any, value: Object): void {
  if (async === this._obj) {
    this._latestValue = value;
    // ... Hidden for the sake of brevity
  }
}

Recap:

So the basic functionality the async pipe provides is taking a Promise or an Observable over it's transform function.

It receives the value or in terms of an Observable multiple values and applies them to _lastValue.

At the end of every transform call _lastValue gets returned.

With this knowledge let's inspect another part of the file.

As a Promise and an Observable can provide a value in the future, we have to tell Angular somehow that the value arrived.

This is done in the _updateLatestValue method with the call of ChangeDetectorRef#markForCheck.

The execution will trigger another template evaluation and the transform gets executed again.

This time it returns as _lastValue the retrieved value from the asynchronous operation.

Unfortunately, the transform gets called with the same instance of the Observable or Promise. This would lead to unexpected behavior.

To avoid it there is a check for distinction in the transform function. Those checks also deal with disposing of the passed object and start receiving values from the new object.

Also, the type checks and undefined values are handled there.

// https://github.com/angular/angular/blob/master/packages/common/src/pipes/async_pipe.ts#L91-L111
transform(obj: Observable<any>|Promise<any>|null|undefined): any {
if (!this._obj) {
  // ... Hidden for the sake of brevity
  return this._latestValue;
}
if (obj !== this._obj) {
  // ... Hidden for the sake of brevity
  return this._latestValue;
}
if (ɵlooseIdentical(this._latestValue, this._latestReturnedValue)) {
  // ... Hidden for the sake of brevity
  return this._latestValue;
}
  return this._latestValue;
}

Recap:

What the async pipe provides us, is taking a Promise or an Observable over it's transform function.
It renders the last value of the passed Promise or Observable and also switches to a newly passed Promise or an Observable and handles the dispose logic for the old instance.

The style of the original source code is very imperative and does not leverage the power of RxJS.

The reason is that the Angular team had strict rules regarding the use of RxJS operators and avoided them wherever possible, to not rely on third party libs and in terms of bundle size.

In this tweet I showed how the codebase could look like if RxJS operators would be used:

async pipe - Imperative vs Reactive



We could reduce the number of lines from 84 to 28 and reduced the complexity of the code drastically.

Now we could also implement configurations without a lot of effort.

@Pipe({name: 'async', pure: false})
export class AsyncPipe implements OnDestroy, PipeTransform {
value: any = null;
subscription;
observablesToSubscribeSubject = new Subject<Observable<any>>();
obs$ = this.observablesToSubscribeSubject
  .pipe(
    distinctUntilChanged(ɵlooseIdentical),
    switchAll(),
    distinctUntilChanged(),
    tap(v => { this.value = v; this.ref.markForCheck(); })
  );

constructor(private ref: ChangeDetectorRef) {
  this.subscription = this.obs$.subscribe();
}

ngOnDestroy(): void {
  this.subscription.unsubscribe();
}

transform(obj: Observable<any> | Promise<any> | null | undefined): any {
  this.observablesToSubscribeSubject.next(toObservable(obj));
  return WrappedValue.wrap(this.value);
  function toObservable(obj) {
    if (ɵisObservable(obj) || ɵisPromise(obj))
      return from(obj);
    else
      throwError(new Error('invalidPipeArgumentError'));
    }
  }
}

Most of the logic is taken away by the distinctUntilChange operator. Also, this operator helps to not render the same value twice.

And the switchAll operator.

To implement a first version of the push pipe we need to change only one line of code.

We change markForCheck

tap(v => { this.value = v; this.ref.markForCheck(); })

to detectChanges

tap(v => { this.value = v; this.ref.detectChanges(); })
VoilĂ !

We are now ready to tackle all the tricky problems to make it work at scale and with all edge cases considered! ;)

Summary

Angular provides us a way to bind observables directly to the template, which triggers rendering on every new value coming from the passed observable.

From the theory section on rendering and ChangeDetectorRef#markForCheck we realized that the way the application renders the change has a bad impact on performance, due to its inefficient rendering mechanism.

As we refactored to a more reactive way we could reduce the codebase, be more flexible with new behavior and the rendering is a little bit more performant (don’t render the same value twice).

However, we also learned about new ways to run change detection only locally.
But to leverage local change detection we also have to overcome a set of tricky problems.

As we have a clear understanding of the implementation details and refactored the codebase to a reactive style let's think about the push pipe and its possible implementation in part 2.

đź“ť New possibilities with Angular's push pipe - Part 2