DEV Community

Cover image for Boost Your App's Performance by Wrapping Your Functions Inside a Pipe
thomas for This is Angular

Posted on • Updated on • Originally published at Medium

Boost Your App's Performance by Wrapping Your Functions Inside a Pipe

The purpose of this article is to provide you with a small, strongly-typed utility pipe that you can easily incorporate into your project to improve its performance.

This article will also provide the solution for challenge #9 of Angular Challenges, which is intended for intermediate-level developers seeking to enhance their understanding of pipes. If you haven’t tried it yet, I encourage you to do so before coming back to compare your solution with mine. (You can also submit a PR that I’ll review)


In this article, we will not delve into the workings of pipes or how they can benefit your Angular application. If you wish to learn more about these topics, please refer to my previous article.

As mentioned in the previous article, calling a function inside a template can have a significant impact on performance as the function will be recomputed every time change detection is executed. We can mitigate this by adding a memo function to cache the input value, as described in the above article. However, this approach has a limitation in that we can only use the memo function once per input.

The solution to this problem is to move our function inside a pipe. However, if we have multiple functions to move, we would need to create a new pipe for each function, which could become cumbersome.

In challenge #9 of Angular Challenge, we have the following component:

@Component({
  standalone: true,
  imports: [NgFor],
  selector: 'app-root',
  template: `
    <div *ngFor="let person of persons; let index = index; let isFirst = first">
      {{ showName(person.name, index) }}
      {{ isAllowed(person.age, isFirst) }}
    </div>
  `,
})
export class AppComponent {
  persons = [
    { name: 'Toto', age: 10 },
    { name: 'Jack', age: 15 },
    { name: 'John', age: 30 },
  ];

  showName(name: string, index: number) {
    // very heavy computation
    return `${name} - ${index}`;
  }

  isAllowed(age: number, isFirst: boolean) {
    if (isFirst) {
      return 'always allowed';
    } else {
      return age > 25 ? 'allowed' : 'declined';
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The for loop in our code is currently calling two functions for each person in the array. These functions are being recomputed at each change detection cycle, which can cause performance issues.

As mentioned previously, we could create a separate ShowNamePipe and IsAllowedPipe for each function, but this would require creating a large number of pipes for every component in our application.

To solve this problem, we can wrap our function inside a generic pipe called WrapFnPipe, as shown below:

@Pipe({
  name: 'wrapFn',
  standalone: true,
})
export class WrapFnPipe implements PipeTransform {
  transform(func: (...arg: any[]) => R, ...args: any[]): R {
    return func(...args);
  }
}
Enter fullscreen mode Exit fullscreen mode

To refactor our HTML with this pipe, the first argument is the function we want to wrap, then we pass all the function arguments separated by :. We can now rewrite our HTML to the following:

<div *ngFor="let person of persons; let index = index; let isFirst = first">
  {{ showName | wrapFn : person.name : index }}
  {{ isAllowed | wrapFn : person.age : isFirst }}
</div>
Enter fullscreen mode Exit fullscreen mode

By using this method, we can obtain all the benefits of Angular pipe without creating a specific pipe for each function. Each function is wrapped inside its own instance of the WrapFnPipe, which means each person has its own cached value. Therefore, when a new change detection is executed, the function is not re-executed and the last cached value is returned, resulting in an immediate performance boost.

However, introducing this wrapping function removes all type safety as it remaps all types to any. To add type safety, let’s explore some options.

A naive approach to adding type safety would be to use generics and replace all any types with a generic type:

transform<ARG, R>(func: (...arg: ARG[]) => R, ...args: ARG[]): R {
  return func(...args);
}
Enter fullscreen mode Exit fullscreen mode

However, this approach will only work if all arguments of the function are of the same type.

codee block error

In our case, we will encounter an error because the generic type ARG only takes the first type, which is string in our case, and not string | undefined. This approach would not be very type safe either because we would be able to invert both arguments.

To address this issue, we need to explore TypeScript’s function overloading feature and take advantage of it. If you are not familiar with function overloading or how it works, you should read the following article first and come back to this article afterwards.

Using function overloading, we end up with this pipe transform function:

@Pipe({
  name: 'wrapFn',
  standalone: true,
})
export class WrapFnPipe implements PipeTransform {
  transform<ARG, R>(func: (arg: ARG) => R, args: ARG): R;
  transform<ARG1, ARG2, R>(
    func: (arg1: ARG1, arg2: ARG2) => R,
    arg1: ARG1,
    arg2: ARG2
  ): R;
  transform<ARG1, ARG2, ARG3, R>(
    func: (arg1: ARG1, arg2: ARG2, arg3: ARG3) => R,
    arg1: ARG1,
    arg2: ARG2,
    arg3: ARG3
  ): R;
  transform<ARG1, ARG2, ARG3, R>(
    func: (arg1: ARG1, arg2: ARG2, arg3: ARG3, ...arg: any[]) => R,
    arg1: ARG1,
    arg2: ARG2,
    arg3: ARG3,
    ...arg: any[]
  ): R;

  transform<R>(func: (...arg: unknown[]) => R, ...args: unknown[]): R {
    return func(...args);
  }
}
Enter fullscreen mode Exit fullscreen mode

Note:

  • The first 4 functions are just definitions for different transform methods. The last one is the actual implementation that combines all of the definitions.
  • The first definition is for a one argument function, the second for a two arguments function and so on. We stop at 4 because it is bad design to have too many arguments, but if you want to, more definitions can be added.

By doing so, we have strong type safety in our template for any function. This includes type safety on the number of parameters, as well as type safety on the type of each input, as shown in the following screenshot:

the number of argument is wrong

The type of the second argument is incorrect


I hope this article has helped you understand the power of pipes and what can be achieved with TypeScript. This pipe is a quick win and can be added to your project right away to boost some very costly operations without the need to refactor everything. 🚀

You can find me on Twitter or Github.

Top comments (3)

Collapse
 
maximlyakhov profile image
maximLyakhov

Thank you! I finally understood places where I shouldn't put generics after the class names.

Collapse
 
divnych profile image
divnych

I've seen a similar implementation, but with the difference that the arguments were used in reverse order. That is, the pipe was applied to the value, and the mapping function was passed as an argument to the pipe. Your solution probably looks better, as it's easier to use with an unlimited number of arguments.

...
transform(value, mappingFn: Function) {
  return mappingFn(value);
}
...
Enter fullscreen mode Exit fullscreen mode

Thanks for sharing.

Collapse
 
achtlos profile image
thomas

Yes you can do it as well and passing the rest of the argument after the name of the function. But it's harder to read on the template, feel less natural.