Efficient impure pipes in Angular
Angular pipes are a useful tool for formatting values for display. However, they may not work as expected for composite objects.
Let us take a look at a made-up example:
export interface ValueWithUnit {
value: number;
unit: string;
}
@Pipe({
name: "formatValue",
})
export class FormatValuePipe implements PipeTransform {
transform(value: ValueWithUnit): string {
return `${value.value} ${value.unit}`;
}
}
The pipe will format the output of a complex object with 2 properties. When used in a component, it will work as expected as long as the object is treated as immutable. If its properties change, the pipe will not be retriggered and the displayed value will not be updated. For example, when the following button is clicked, the view is not updated.
<div>
{{ value | formatValue }}
<button (click)="value.value = value.value + 1">Increment</button>
</div>
The reason for this behavior can be found in the documentation:
Angular detects each change and immediately runs the pipe. This is fine for primitive input values. However, if you change something inside a composite object (such as the month of a date, an element of an array, or an object property), you need to understand how change detection works, and how to use an
impure
pipe.By default, pipes are defined as pure so that Angular executes the pipe only when it detects a pure change to the input value. A pure change is either a change to a primitive input value (such as
String
,Number
,Boolean
, orSymbol
), or a changed object reference (such asDate
,Array
,Function
, orObject
).
As indicated in the quote above, marking the pipe as impure fixes the problem:
@Pipe({
name: "formatValue",
pure: false,
})
export class FormatValuePipe implements PipeTransform {
transform(value: ValueWithUnit): string {
return `${value.value} ${value.unit}`;
}
}
However, this has an unfortunate side effect, as the documentation states:
Angular executes an impure pipe every time it detects a change with every keystroke or mouse movement.
With a simple pipe like the one above, this may not be a problem. However, when the formatting function is more computationally intensive and takes a longer time to execute, this can seriously impact the performance of the application.
In such cases, it is very useful to cache the formatted value in the pipe along with any input that might affect the result. If none of these inputs have changed, you can simply return the cached value. Of course, if the inputs have changed, you still need to recalculate the formatted value and update the cache:
@Pipe({
name: "formatValue",
pure: false,
})
export class FormatValuePipe implements PipeTransform {
private formattedValue: string = "";
private latestValue: number | undefined;
private latestUnit: string | undefined;
transform(value: ValueWithUnit): string {
if (this.latestValue == value.value && this.latestUnit == value.unit) {
return this.formattedValue;
}
this.latestValue = value.value;
this.latestUnit = value.unit;
this.formattedValue = `${value.value} ${value.unit}`;
return this.formattedValue;
}
}
You can find an example project with the above pipe in my GitHub repository. I have added logging to keep track of how many times the returned value is cached and how many times it is recomputed. Even in a trivial application, the cached value can be returned half the time. In a more complex application, the ratio will be even more in favor of the cached value.
By default, Angular pipes are not called when a property in a composite input object changes. In order for them to be called in this case, they must be marked as impure. However, this causes them to be called whenever Angular detects a change. This can be a problem for computationally intensive pipes. To reduce the impact, the output value can be cached so that it does not have to be recalculated each time.