Understanding pipe instantiation and execution in Ivy
Angular’s pipe system is a staple of everyday development. A thorough piece on how pipes work in detail boils down to this:
For a pure pipe, the
transform()method runs only when its input arguments change. Pure is the default setting for pipes.
If a pipe depends on internal state, meaning the output relies on something other than its arguments, setpuretofalse. In that scenario, Angular executes the pipe every change detection cycle, regardless of whether the arguments have shifted.
There’s another notable behavior: Angular generates a single instance for a pure pipe even if that pipe appears multiple times in a template. This was the case in View Engine. Let’s verify whether Ivy preserves this behavior by looking at the internals.
<span [text]=”value | myCustomPurePipe”>
<span [text]=”value | myCustomPurePipe”>
This means myCustomPurePipe should have only one instance in this setup.
With Ivy, we need to check if this still applies, so let’s dive into the engine’s code.
Preparing the environment
Start by creating a new Angular project using version 9 or later, since that’s when Ivy became the default renderer.
ng new study-pipes --style css --skip-tests true --routing false
cd study-pipes
Building a pure and an impure pipe
We’ll define two custom pipes: my-custom-pure-pipe and my-custom-impure-pipe:
ng g pipe my-custom-pure-pipe --skip-tests true
ng g pipe my-custom-impure-pipe --skip-tests true
Update the my-custom-pure-pipe implementation as follows:
@Pipe({
name: 'myCustomPurePipe',
pure: true
})
export class MyCustomPurePipe implements PipeTransform {
constructor() {
console.log('MyCustomPurePipe created');
}
transform(value: number, ...args: any[]): any {
console.log(`MyCustomPurePipe#transform called, value ${value}`);
return value;
}
}
And adjust the my-custom-impure-pipe like this:
@Pipe({
name: 'myCustomImpurePipe',
pure: false
})
export class MyCustomImpurePipe implements PipeTransform {
constructor() {
console.log('MyCustomImpurePipe created');
}
transform(value: number, ...args: any[]): any {
console.log(`MyCustomImpurePipe#transform called, value ${value}`);
return value + value;
}
}
In essence, we’re inserting log statements at two points: when the instance is created, and when Angular invokes transform during change detection.
Modify app.component.ts to match this:
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
number1 = 1;
number2 = 2;
}
In angular.json, locate the aot flag under projects -> study-pipes -> architect -> build -> options -> aot and switch it from true to false. Turning off ahead-of-time compilation lets us inspect the emitted code without extra complexity.
With the setup complete, it’s time to investigate.
Examining how pipes behave
Assume your app.component.html contains this template:
<span>{{ number1 | myCustomPurePipe }}</span>
<span>{{ number2 | myCustomPurePipe }}</span>
<span>{{ number1 | myCustomImpurePipe }}</span>
<span>{{ number2 | myCustomImpurePipe }}</span>
Open Chrome dev tools, go to the Sources tab, and click on the app component file. You’ll see the compiled output:

Within AppComponent_Template, there are two distinct if sections: one for the initial creation phase rf & 1, and another for change detection updates rf & 2.
Here’s the block responsible for creation:
if (rf & 1) { // this is the creation phase
jit___elementStart_2(0,'span');
jit___text_3(1);
jit___pipe_4(2,'myCustomPurePipe'); // pipe instance is created
jit___elementEnd_5();
jit___elementStart_2(3,'span');
jit___text_3(4);
jit___pipe_4(5,'myCustomPurePipe'); // pipe instance is created
jit___elementEnd_5();
jit___elementStart_2(6,'span');
jit___text_3(7);
jit___pipe_4(8,'myCustomImpurePipe'); // pipe instance is created
jit___elementEnd_5();
jit___elementStart_2(9,'span');
jit___text_3(10);
jit___pipe_4(11,'myCustomImpurePipe'); // pipe instance is created
jit___elementEnd_5();
The jit__pipe_4 function is what Angular uses to instantiate a pipe. As you can see, four pipe instances are created. This reveals that in Ivy, each pipe usage gets its own instance—whether it’s pure or impure. That contrasts with View Engine, where a pure pipe was reused across usages.
Now, let’s review the change detection segment:
if (rf & 2) {
jit___advance_6(1);
jit___textInterpolate_7(jit___pipeBind1_8(2,4,ctx.number1));
jit___advance_6(3);
jit___textInterpolate_7(jit___pipeBind1_8(5,6,ctx.number2));
jit___advance_6(3);
jit___textInterpolate_7(jit___pipeBind1_8(8,8,ctx.number1));
jit___advance_6(3);
jit___textInterpolate_7(jit___pipeBind1_8(11,10,ctx.number2));
}
Here, the jit___pipeBind1_8 function handles calling transform on the pipe.
The actual implementation can be found in the source code:
// this code is called in update phase, or when change detection runs
export function ɵɵpipeBind1(index: number, slotOffset: number, v1: any): any {
const adjustedIndex = index + HEADER_OFFSET;
// get LView, LView stands for Logical View
const lView = getLView();
// get pipeInstance from LView
const pipeInstance = load<PipeTransform>(lView, adjustedIndex);
return unwrapValue(
lView,
// whether pipe is pure
isPure(lView, adjustedIndex) ?
// call pipe’s transform method or return from cache value
pureFunction1Internal(
lView, getBindingRoot(), slotOffset, pipeInstance.transform, v1, pipeInstance) :
// pipe is impure then call pipe’s transform method directly
pipeInstance.transform(v1));
}
The isPure check determines the pipe’s type by inspecting the pure property in the @Pipe decorator.
With impure pipes, Angular invokes transform every change detection cycle. For pure pipes, transform runs only when the inputs change; otherwise, a previously computed value is returned.
Wrapping up
Here’s a breakdown of the key findings:
- In Ivy, each pipe instance is unique to its usage, even for pure pipes. View Engine, by contrast, shared a single instance for pure pipes. For example, using
myCustomPurePipetwice in a template under Ivy results in two separate instances. - With the default change detection strategy, an impure pipe triggers a
transformcall on every cycle. A pure pipe only callstransformwhen its input arguments have changed since the last invocation. If nothing changed, the cached result from the prior call is reused. - If you’re using the impure pipe
async, pair it with OnPush change detection to avoidtransformbeing executed needlessly on each cycle.
There are StackBlitz examples to experiment with: one for View Engine and another for Ivy.
Enjoy exploring!
