Skip to main content

Basic strategies

Concepts

Render work

To apply changes to a component template we need to re-evaluate the template. Internally this is done in the specific strategy. This process will execute whenever a component's template is re-evaluated through async, push, ChangedDetectorRef.detectChanges or a structural directive template is re-evaluated through EmbeddedView.detectChanges.

It can be pretty time consuming and directly depends on the following factors:

  • HTML size (only for init and destroy and bundle-size)
  • JS size (only for init and destroy and bundle-size)
  • number of event bindings (only for init and destroy)
  • number of template bindings/expressions
  • number and size of child-components
  • number of directives (especially ngFor and nested structures)

Some of the problems related to work of Angular are:

Out of bound change detection: If we perform this re-evaluation without any visual change for the user (over-rendering) we introduce noticeable performance degradations.

The out of bound change detection can be caused through:

  • Zone pollution
  • Missing ChangeDetectionStrategy.OnPush
  • Component template projection
  • Pull-based rendering processes

Out of bound template evaluation: If we perform a re-evaluation of a single property in the template any other expression/binding also gets re-evaluated. Again over-rendering introduces noticeable performance degradations.

The out of bound template evaluation can be caused through:

  • Pull-based rendering processes
  • any reactive change through async
  • any call to cdRef.detectChanges

Work performed for out of viewport content: If we perform a re-evaluation or even re-rendering of the DOM of elements outside of the viewport we perform useless work for the user. This will pollute the main thread and reduced time for more important content to get rendered.

The re-evaluation or browser re-rendering can be caused by:

  • Bad style changes
  • Big LCP (Largest Contentful Paint) elements
  • Large amount of content

Local vs global CD

ChangeDetection

The change detection system that is currently implemented in Angular is pull-based, but way more important, as a side effect it also runs CD globally. It performs a re-rendering where at optimum every single component on the path from the root to the actual UI update needs to get re-evaluated. A lot of performed work is useless.

Technically the methods to run change detection are markForCheck / markViewDirty, ɵmarkDirty and tick.

If we want to avoid this process we can run change detection locally and re-render only the very component and potentially its children.

Technically the methods we can use for it are detectChanges or ɵdetectChanges

Render Strategies-global-vs-local

Pull vs push based

Render Strategies-request-subscribe Consuming value changes can be done by constantly watching the source for changes and pull them, or subscribe to the changes like a DOM event binding once and get the changes pushed.

In a simple setup the pull might be a quick solution and you just .get() the value, but a push based architecture always scales better.

Compare it with HTTP calls vs WebSockets.

If we apply this concepts to our change detection mechanics we can directly apply changes where they are need and skip nearly all the unnecessary work.

In combination with Observables, and EmbeddedViews change detection can be speed up dramatically by this architecture.

Render Strategies-pull-vs-push

Strategies

NamePriorityRender MethodSchedulingRender Deadline
"native"markForCheckrequestAnimationFrameN/A
"local"🠗 detectChangesrequestAnimationFrameN/A
"noop"- nooprequestAnimationFrameN/A

Native

rx-angular-cdk-render-strategies__strategy-native

This strategy mirrors Angular's built-in async pipe. This means for every emitted value ChangeDetectorRef#markForCheck is called. Angular still needs zone.js to trigger the ApplicationRef#tick to re-render, as the internally called function markViewDirty is only responsible for dirty marking and not rendering.

NameZone AgnosticRender MethodCoalescingScheduling
nativemarkForCheck✔ RootContextrequestAnimationFrame

Local

This strategy is rendering the actual component and all its children that are on a path that is marked as dirty or has components with ChangeDetectionStrategy.Default.

rx-angular-cdk-render-strategies__strategy-local

As detectChanges has no coalescing of render calls like ChangeDetectorRef#markForCheck or ɵmarkDirty have, we apply our own coalescing, 'scoped' on component level.

Coalescing, in this very manner, means collecting all events in the same EventLoop tick, that would cause a re-render. Then execute re-rendering only once.

'Scoped' coalescing, in addition, means grouping the collected events by a specific context. E. g. the component from which the re-rendering was initiated.

This context could be the Component instance or a ViewContextRef, both accessed over the context over ChangeDetectorRef#context.

NameZone AgnosticRender MethodCoalescingScheduling
local🠗 detectChanges✔ ComponentContextrequestAnimationFrame

The best place to use the local strategy is a structural directive like *rxLet. Those will have a independent template from the component and perform changes only there.

render-strategies - basic-strategies - local - directive_michael-hladky

This has a pretty nice performance boost and is causing only minimal change detection work.

Noop

The no-operation strategy does nothing. It can be a valuable tool for performance improvements as well as debugging.

rx-angular-cdk-render-strategies__strategy-noop

NameZone AgnosticRender MethodCoalescingScheduling
noop- noop

Usage

Component / Service

import { RxStrategyProvider } from '@rx-angular/cdk/render-strategies';

@Component()
class Component {
constructor(private strategyProvider: RxStrategyProvider) {
strategyProvider.schedule(() => {}, { strategyName: 'local' });
}
}

Template

import { RxLet } from '@rx-angular/template/let';
import { RxFor } from '@rx-angular/template/for';
import { RxPush } from '@rx-angular/template/push';

@Module({
imports: [RxLet, RxFor, RxPush],
})
class Module {}
<h1 *rxLet="title$; strategy:'local'">{{title}}</h1>
<a *rxFor="let item of items$; strategy:'native'">{{item}}</a>
<p>{{title$ | push : 'local'}}</p>