Angular Performance: Optimizing Expression Re-evaluation with Pure Pipes

Learn what Angular pipes are and how to use them wisely to optimize your app’s performance

Chidume Nnamdi 🔥💻🎵🎮
Bits and Pieces

--

Pipes is an unpopular but very essential feature of Angular. It enables us to pipe our data and modify it on-the-go, without adding extra code to our components; the data is piped through and transformed before being displayed.

For example, we have an array of data and we want to transform the array before it is displayed on a table but we don’t want the transformation to affect the class it is on, here ‘Pipe’ is the best option to use.

pipe
------------
[1,3] ---> | x * 2 | ---> [2, 6]
------------

The diagram above illustrates how pipes works, see the array [1,3] was passed through the pipe, inside it, the array was transformed by multiplying the elements by 2, the pipe will output the array with the multiples of 2.

Tip: Build faster with shared components using bit.dev component hub. It lets you easily share your components across apps, suggest updates from any projects, sync changes- and build faster with reusable components. Try it.

NG components with Bit — Instantly share, reuse and develop anywhere
class Ar {
constructor(arr) {
this.arr = arr
}
pipe(...fns) {
let result = this.arr
for (var index = 0; index < fns.length; index++) {
var fn = fns[index];
result = result.map((el) => fn(el))
}
return result
}
}
const a = new Ar([3, 4, 5, 6])log(a.arr)
// [3, 4, 5, 6]
log(a.pipe(
(elm) => elm * 8,
(elm) => elm + 18
))
log(a.arr)
// [3, 4, 5, 6]
$ node ngc
[ 42, 50, 58, 66 ]

The above class ‘Ar’ has an instance variable ‘arr’ which is an array. We want to transform the array before we can display it using console.log and we don't want the transformation to affect the arr variable in the class. So we created a pipe method which takes in an array of transformation functions, it passes the arr array through the functions, piping the result of each function to the next function inline; when done, the final result is returned without modifying the arr variable.

We see the same in RxJS, we can pipe the stream through many operators transforming the stream without modifying the original values of the stream.

const stream = Rx.Observable.create([9,8,6,7,5])
stream.pipe(
map(el => {s: el*4})
).subscribe((v)=> log(v))
// outputs:
{s: 36}
{s: 32}
{s: 24}
{s: 28}
{s: 20}

We created a stream of Observable to emit the values in the array [9,8,6,7,5], then we pass each of them through the map operator which modifies each of the values by multiplying them by 4 and returns an object.

In Angular, we use the | symbol in our component template to tell Angular when we need to pipe some data.

@Component({
// ...
template: `
My name is {{ 'nnamdi chidume' | uppercase }}
`
})
export class App {}

See, we passed the string nnamdi chidume to the uppercase pipe. We want Angular, on resolving our views, to pass the string to the uppercase pipe to transform the string, well… to uppercase.

On rendering it will look like this:

My name is NNAMDI CHIDUME

Angular has several in-built pipes:

  • lowercase: To transform uppercase characters to lowercase.
  • date: To transform a date to the format we want.
  • json: to transform a data to a JSON format.
  • uppercase: to transform lowercase characters to uppercase characters.
  • currency: To convert a currency to another currency equivalence.
  • percent: To format a number as percentage.

We can build our own custom pipe by implementing Angular’s Pipe class!

@Pipe({
name: 'custom_pipe
})
class CustomPipe implements PipeTransform {
transform(value: any, args?: any): any {
// ...
}
}

We used the @Pipe decorator to tell Angular that the class is a pipe and should be used like such. The name property in the decorator tells Angular the name of our pipe to be used against the | symbol, in our component template.

Our CustomPipe class implements the PipeTransform interface. The ‘transform’ method is where we put out customized logic. Angular runs the transform whenever it encounters our custom pipe in a template, passing the data after the | symbol to the method.

We can make our CustomPipe to always append nnamdi to values passed to it:

@Pipe({
name: 'custom_pipe
})
class CustomPipe implements PipeTransform {
transform(value: any, args?: any): any {
return value + " nnamdi"
}
}

In our component template:

Custom Pipe: {{'john' | custom_pipe}}

Output:

Custom Pipe: john nnamdi

Now we know what pipe is in Angular, how they work and how to create one.

Let’s get down to how they can really affect the performance of our apps if not used properly.

Case Study

Let’s say we have an app that renders a list of names and ages of people in the same area.

@Component({
selector: 'listOfPeople',
template: `
<div class="container">
<div *ngFor="let person of people">
Name: {{ person.name}}
Age: {{person.age}}
Fibo of Age: {{fib(age)}}
</div>
</div>
`,
changeDetectionStrategy: OnPush
})
export class ListOfPeople {
@Input() people: Array<any>;
fib(num) {
if(num == 1 || num ==2 ) return 1
return fib(num -1 ) + fib(num - 2)
}
}

This component is a pure component that receives an array from its parent component. Whenever the input people changes, the component will re-render to reflect the new changes. This is so because it has the OnPush detection strategy. This detection strategy runs a CD on a component if the input of the component changes reference. If not, the component is not rendered. That CD will also trigger template expressions on the template.

In our above component, we have a function being called in our template fib(...) among other template expressions:

  • {{ person.name}}
  • {{person.age}}

When we feed input to ListOfPeople component like this:

@Component({
selector: 'peopleComp',
template: `
<div>
<listOfPeople [people]="peopleList"></listOfPeople>
<div>
<input #in placeholder="Type name here..." />
<button (click)="addNewUser(in.target.value)">Add New Person</button>
</div>
</div>
`
})
export class PeopleComponent {
peopleList = [
{
name: 'nnamdi',
age: 90
},
//... 20 records
]
addNewUser(user) {
this.peopleList = this.peopleList.concat([
{
name: user,
age: Date.now()
}
])
}
}

Whenever we click the Add New User button, a new user is added to the peopleList array which is fed to ListOfPEaople component. The LisOfPeople component sees a new reference and iterates over the list via *ngFor. Now, for each record in the list, the template expression fib() is called. Let's say we have a total of 100 records and we paginate to 20 records; That means that the fib will be called 20 times for each page. Not only whenever we add a new record, but a CD will also run on ListOfPeople component and the rendering will cause the fib method to run 20 times again!!

Let’s say our fib function takes 3s to execute; It would take (3*20) 60~ secs for our component to render. The initial rendering might be negligible but upon subsequent re-renderings, the fib function should not be called on previous records, it should be called on the new record. So we see that on re-rendering we will be executing 20 useless fib calculations.

Let’s not rule our the initial render, if the same input is fed to the fib function on the initial render, the fib calculation should be done on the first input and the result returned on the input with the same value, no point re-calculating on the same input it has seen before.

This is what we need to do:

  1. Improve initial rendering performance: prevent re-calculation of the same input previously seen.
  2. Prevent recalculation of fib function upon re-rendering
  3. Calculate only on the newly added record

First, we have to deal with calling functions on templates, this is not a recommended way in Angular because the function will always be called whenever CD is run on the component. Because of this, it is considered best to substitute function call with a pipe call.

Integrating pipe will make us limit updates in our template bindings, but with a function call, the template bindings will always be updated. Angular pipes are of two types:

  • Impure
  • Pure

Impure pipe: This pipe produces side-effects. They affect the general global state of the app. When called with a certain input produces a different output.

pure pipe: This produces an output based on the input it was given without no side-effect. Pure pipes will only run its logic in the transform method whenever the input changes if it receives an input same as before there will be no action,

A pure pipe is only re-evaluated when either the inputs or any of the arguments change.

So how does this matter in our needs to optimize our template evaluations using pipes?

We don’t want to evaluate and call expressions whenever a component re-renders all over again and we don’t want to perform a calculation on the same input it has seen before. So now we see that pure pipe is the answer to our optimization needs.

We can now create a pipe (a pure one) and encapsulate our fib function in it:

function fib(num) {
if(num == 1 || num ==2 ) return 1
return fib(num -1 ) + fib(num - 2)
}
@Pipe({
name: 'fib',
pure: true
})
export class FibPipe implements PipeTransform {
transform(val: any, args: any) {
return fib(val)
}
}

We have something we haven’t seen before he pure option. This tells angular that our pipe is a pure pipe and should be treated as one. See in our transform method we called and returned the value of the fib function.

So we can use it in our component templates like this:

@Component({
selector: 'listOfPeople',
template: `
<div class="container">
<div *ngFor="let person of people">
Name: {{ person.name}}
Age: {{person.age}}
Fibo of Age: {{age | fib}}
</div>
</div>
`,
changeDetectionStrategy: OnPush
})
export class ListOfPeople {
@Input() people: Array<any>;
}

The rendering time of our component will decrease drastically.

It’s like we have solved the issue, yes but there is more. Angular says pure pipes only runs the pipe when the input of the previous input is not the same.

Let’s say we have this data in the array to be rendered by ListOfPeople component:

[
{
name: 'nnamdi',
age: 90
},
{
name: 'chidume',
age: 80
},
{
name: 'david',
age: 90
}
]

On rendering, it will be like this

Name: {{ 'nnamdi'}}
Age: {{90}}
Fibo of Age: {{90 | fib}}
Name: {{ 'chidume'}}
Age: {{80}}
Fibo of Age: {{80 | fib}}
Name: {{ 'david'}}
Age: {{90}}
Fibo of Age: {{90 | fib}}

The first record nnamdi Angular runs the FibPipe because its last input is nothing it hasn't seen any. On the chidume record it will run the Fib pipe because the last input it saw was 90 not the same as the present value. The last record david the Fib of it will be evaluated because the last value is 80.

We see that this:

Name: {{ 'nnamdi'}}
Age: {{90}}
Fibo of Age: {{90 | fib}}
... Name: {{ 'david'}}
Age: {{90}}
Fibo of Age: {{90 | fib}}

will be evaluated twice on initial rendering, on subsequent re-rendering, it won’t be evaluated.

The problem is on the initial rendering, how do we make it remember previous? We will achieve it through memoization.

Memoizing pure pipes

What is memoization? Memoizatoin is the process of storing the result of input and returning the stored when the same input is passed in again.

We need to memoize the fib pipe so that we won’t have an evaluation of inputs previously seen.

The lodash and underscore libraries exports memoizing functions but we are going to implement our own memoizing function and use it.

function memoize(fn: any) {
return function () {
var args =
Array.prototype.slice.call(arguments)
fn.cache = fn.cache || {};
return fn.cache[args] ? fn.cache[args] : (fn.cache[args] = fn.apply(this,args))
}
}

We will now wrap the fib function in this memoize function.

const memoizedFib = memoize(fib)

memoize returns a higher-order function that is a memoized version of the passed in function.

function fib(num) {
if(num == 1 || num ==2 ) return 1
return fib(num -1 ) + fib(num - 2)
}
const memoizedFib = memoize(fib)
@Pipe({
name: 'fib',
pure: true
})
export class FibPipe implements PipeTransform {
transform(val: any, args: any) {
return memoizedFib(val)
}
}

Using Decorators

We can use this new feature to add memoization to our pipe transform function like we use @Pi[e decorator that how we will use it to add memoization to the transform method.]

First, we create the decorator function:

function pure () {
return function(target: any, key: any, descriptor: any) {
const oldFunc = descriptor.value
const newFunc = memoize(oldFunc)
descriptor.value=function() {
return newFunc.apply(this,arguments)
}
}
}

Then we apply it to our FibPipe class:

function fib(num) {
if(num == 1 || num ==2 ) return 1
return fib(num -1 ) + fib(num - 2)
}
@Pipe({
name: 'fib',
pure: true
})
export class FibPipe implements PipeTransform {
@pure()
transform(val: any, args: any) {
return fib(val)
}
}

We should be careful when using memoization, it comes at a great cost. Arguments can be heavy, complex objects might be passed to our memoized function and there will be a heavy memory consumption in order to cache the previous arguments. We should use it carefully because the garbage collector doesn’t t clean up the memory occupied by the cached results.

Conclusion

We learned what Pipe is all about in Angular. How they work, how to use them, how to create our own custom pipes and how to optimize them to achieve peak performance.

We used pure pipes to stave off unnecessary calculations and DOM updates in components, then, we noticed that during initial rendering that calculations are done for inputs with the same value. So we employed memoization to cache results and return when the input occurs again.

With all this information, you can safely add Pipes to your Angular projects knowing fully what you are doing.

If you have any question regarding this or if you think there’s anything I should add, correct or remove, feel free to comment, email or DM me

Thanks !!!

Learn More

--

--

JS | Blockchain dev | Author of “Understanding JavaScript” and “Array Methods in JavaScript” - https://app.gumroad.com/chidumennamdi 📕