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, set pure to false. 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:

How pure and impure pipes work in Angular Ivy — figure 1

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 myCustomPurePipe twice in a template under Ivy results in two separate instances.
  • With the default change detection strategy, an impure pipe triggers a transform call on every cycle. A pure pipe only calls transform when 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 avoid transform being executed needlessly on each cycle.

There are StackBlitz examples to experiment with: one for View Engine and another for Ivy.

Enjoy exploring!