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
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
Pull vs push based
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.
Strategies
Name | Priority | Render Method | Scheduling | Render Deadline |
---|---|---|---|---|
"native" | ❌ | ⮁ markForCheck | requestAnimationFrame | N/A |
"local" | ❌ | 🠗 detectChanges | requestAnimationFrame | N/A |
"noop" | ❌ | - noop | requestAnimationFrame | N/A |
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.
Name | Zone Agnostic | Render Method | Coalescing | Scheduling |
---|---|---|---|---|
native | ❌ | ⮁ markForCheck | ✔ RootContext | requestAnimationFrame |
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
.
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
.
Name | Zone Agnostic | Render Method | Coalescing | Scheduling |
---|---|---|---|---|
local | ✔ | 🠗 detectChanges | ✔ ComponentContext | requestAnimationFrame |
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.
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.
Name | Zone Agnostic | Render Method | Coalescing | Scheduling |
---|---|---|---|---|
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>