The core problem

Whenever change detection fires in Angular — whether triggered by user interactions, timers, manual calls, or anything else — every expression in the template gets evaluated again, including function calls. This is by design: Angular needs to re-evaluate everything to detect what has changed and update the DOM accordingly.

The performance concern arises when these function calls involve heavy computation. Because Angular's view update process runs synchronously, it must wait for each function to finish before proceeding, which can cause noticeable lag in the application.

The conventional advice: use pipes

The typical recommendation is to replace function calls with pipes. The rationale is straightforward: pipes in Angular templates are more efficient. The key difference is that a pipe's transform method only re-executes when the arguments passed to it change.

Let’s verify this claim by examining the internals.

Looking under the hood

With the Ivy compiler, Angular processes templates into a set of compiled instructions. Consider a basic component that uses a pipe — here's a simplified version of the generated code:

On line 29 of the generated output, we see the template instructions being laid out. There are calls for creating a div, setting text content, registering a pipe, closing the element, and attaching a button with a click listener. The if/else block separates the initial view creation from subsequent view updates. More details on this can be found in the official documentation.

The parts we care about are line 33 and line 43. The first registers the pipe during setup; the second handles the data binding during the update phase. Notice the call to ɵɵpipeBind1() — this is where the pipe's output is computed. Let’s investigate what this function actually does.

Link to source code [here](https://github.com/angular/angular/blob/main/packages/core/src/render3/pipe.ts#L123)

The first three lines retrieve the pipe instance. Then comes the return statement, which checks isPure() — this flag comes directly from the pure property in the pipe's decorator configuration.

If the pipe is marked as impure (pure: false), it immediately returns pipeInstance.transform(v1), meaning Angular simply re-runs the transform logic every time without any optimization — essentially equivalent to calling a regular method on the component class.

For a pure pipe (the default), Angular delegates to a utility called pureFunction1Internal. Here’s what that helper looks like:

Link to source code [here](https://github.com/angular/angular/blob/main/packages/core/src/render3/pure_function.ts#L307)

It starts by calling bindingUpdated to check whether the bound value has changed. If the binding is stale, it updates it; otherwise, it skips re-computation and reuses the previous result. Let’s look at what bindingUpdated actually validates:

Link to source code [here](https://github.com/angular/angular/blob/main/packages/core/src/render3/bindings.ts#L46)

The check boils down to comparing the old and new values using Object.is() equality. The value in question is the argument passed to the pipe's transform method. If none of the arguments have changed, the function returns false, and Angular avoids re-running the transform — it simply reuses the cached output from the previous change detection cycle.

That’s precisely the behavior we wanted to confirm.

What does this mean in practice?

This opens the door for a small utility that can replicate this caching mechanism without needing to define a custom pipe. Here’s a helper function that does exactly that:

In essence, memo is a function that takes another function as input and returns its cached result. On line 6, it uses hasDifferentArgs to determine if the arguments have changed. This helper first compares the length of the arguments; if they differ, it returns true. Otherwise, it performs an equality check on each argument individually — just like the pipe mechanism does.

If the arguments are different, the original function is invoked with the new parameters, and the result is stored. If they’re the same, the previously computed value is returned without re-executing the function.

Using the memo helper

Here’s how the earlier example looks when adapted to use memo:

That’s it. A function call in the template, and no performance penalty — as long as you use it correctly.

Are there any limitations?

Yes, one important caveat: the memo function works well only when the same arguments are passed to it each time. If you use it with different arguments in different parts of the template, the cache will be overwritten on each call.

Here’s an illustration of what can go wrong:

In the example above, because different values are passed in different template positions, the “isOdd called” log will appear six times per click, as the cache is only maintained globally per function call, not per usage point.

It’s ok to use function calls in Angular templates! — figure 4

Conversely, if you pass the same value at all usage sites, the function will only execute once.

And here’s the corresponding console output:

It’s ok to use function calls in Angular templates! — figure 5

Pipes cache results per use in the template, whereas the memo utility caches per function definition.

Where did this idea come from?

The inspiration for this exploration came from a tweet by Pawel Kozlowski, a member of the Angular core team. His post prompted me to dive into the framework’s internals and expand on the concepts.

If you enjoy topics like reactivity, signals, performance, and change detection, be sure to follow Pawel on Twitter — he regularly shares valuable insights in this space.

And while you’re there, feel free to follow me at @Enea_Jahollari for the latest Angular news, videos, podcasts, RFCs, and pull requests. If you found this article useful, follow me on dev.to for more content like this.

Thanks for reading!