Когда я только начал осваивать Angular, мне не было до конца понятно, что это за зверь такой — Pipe, и зачем вообще его использовать. Официальная документация гласит следующее (в переводе от metanit.com):

Pipes представляют специальные инструменты, которые позволяют форматировать отображаемые значения

Да, это действительно объясняет, что делают Pipes. Но в тоже время не отвечает на вопрос, а зачем их использовать? Почему нельзя воспользоваться обычным методом класса? Pipes для меня какое-то время был тёмной лошадкой. Но потом я узнал «страшный секрет» о Pipes, который всё расставил на свои места…

Ладно, я излишне накручиваю интригу. Главный секрет Pipes заключается в том, что они кэшируют результат трансформации. Для опытного разработчика это никакой не секрет. Но для меня это было неочевидно. Да и сегодня замечаю, что не все мои коллеги в курсе этой особенности. Не понимаю, почему в документации кэширование так вскользь затронуто. Может быть, разработчики не особо хотят говорить на неудобные темы (Change Detection)? Так или иначе, кэширование в Pipes меняет всё, и я хотел бы рассказать в этой небольшой статейке, почему использовать Pipes — важно

⚠️ В чем проблема

Для начала хотелось бы затронуть тему Change Detection в Angular, ведь именно этот механизм мотивирует использовать Pipes. Говоря кратко, на каждый чих Angular запускает проверку изменений. Она сравнивает все значения в компоненте, в том числе дёргает getter’ы и вызывает функции, если они есть в шаблоне. И в этом заключается суть проблемы. Допустим, у вас есть следующий код:

<ul>
@for (fruit of fruits.split(', '); track fruit) {
  <li>{{ fruit }}</li>
}
</ul>

Он кажется безобидным, но заключает в себе серьезную проблему — при каждый проверке изменений, будет выполняться split(‘, ’)! В приложении может быть очень много триггеров на проверку изменения состояния. Например, в проекте над которым я сейчас работаю, за секунду может триггернуться 100–200 проверок (привет, ChangeDetectionStrategy.Default). И каждый раз выполнять split? Это накладно. Но тут на помощь приходят пайпы

? Как использовать Pipes

Создадим сам пайп через консольную команду npx ng generate pipe split

@Pipe({ name: 'split' })
export class SplitPipe implements PipeTransform {
  transform(value: string): string[] {
    return value.split(', ');
  }
}

И отрефакторим шаблон следующим образом:

<ul>
@for (fruit of fruits | split:', '; track fruit) {
  <li>{{ fruit }}</li>
}
</ul>

Теперь трансформация вызовется всего один раз. Все благодаря тому, что мы написали чистый Pipe, который будет выдавать закэшированный результат трансформации до тех пор, пока передаваемое в него значение не изменится (или ссылка в случае, если в Pipe кладется объект)

Поведение Pipe можно изменить, если установить в директиве @Pipe({ ..., pure: false }). Тогда Pipe будет вести себя как обычный сервис и осуществлять новую трансформацию при каждой проверке изменений (но зачем это делать?)

В Angular уже встроены наиболее распространенные Pipes. Например, мне пригождались SlicePipe, DatePipe, LowerCasePipe и UpperCasePipe из стандартной библиотеки. Подробнее о них можно узнать тут.

Но их не так много. Поэтому, скорее всего вам придется написать свои Pipes, либо воспользоваться какой-либо существующей библиотекой. Например, @nglrx/pipes (ссылка). В ней реализованы десятки стандартных операций для строк, чисел и массивов, в том числе и split из примера

⏰ «Нечистые» Pipes

Изначально я не затрагивал тему AsyncPipe в статье, поскольку на моей практике не возникало по нему вопросов. Но как заметил @Ashita (большое спасибо за идею!), это было досадное упущение, ведь этот пайп не только очень распространен, но и позволяет раскрыть тему «нечистых» ( pure: false ) Pipes

Несмотря на массовый переход на Signals, скорее всего вам придется столкнуться с RxJs. Эта библиотека обертывает реактивные значения в Observable<T>, из которых вытащить значение в шаблоне несколько накладно… Но на помощь приходит AsyncPipe

fruits$ = of(this.fruits.split(', '));
<ul>
@for (fruit of fruits$ | async; track fruit) {
  <li>{{ fruit }}</li>
}
</ul>

Выглядит как уличная магия. Но на удивление AsyncPipe довольно органично вписывается в концепцию Pipes. Дело в том, что AsyncPipe наследует ChangeDetectorRef. Благодаря нему, в момент, когда по подписке на Observable<T> прилетает новое значение, AsyncPipe оповещает механизм проверки изменений о нём

К сожалению, механизмы Angular'а не могут полагаться на закэшированное значение для такого пайпа, ведь переданный объект (он же Observable<T>) не изменяется. Поэтому AsyncPipe реализован «нечистым»: трансформация будет вызываться каждый раз при проверке состояния. Однако не стоит переживать за производительность, ведь в данном случае кэширование реализовано на уровне самого AsyncPipe

✅️ Когда использовать Pipes

Механизм проверки состояний всегда был слабым местом фреймворка Angular. И очевидно, Pipes были придуманы, чтобы «закостылить» случаи, когда всё становится совсем плохо. К счастью, избегать такие ситуации довольно легко, особенно с Signals. Но для себя я всё же выделил следующие три кейса, когда Pipes могут быть полезны:

1️⃣ Преобразование внутри @for

В идеале мы передаем в @for уже полностью обработанный массив. На практике же это не всегда удобно и проще вызвать преобразование внутри @for

<ul>
@for (fruit of fruitList; track fruit) {
  <li>{{ fruit | lowercase }}</li>
}
</ul>

2️⃣ Тривиальные, но ресурсоемкие преобразования

Вам вряд ли захочется реализовать самостоятельно кэшировние ради одного лишь slice или toUpperCase. К счастью, для этого можно воспользоваться Pipes. Еще раз упомяну @nglrx/pipes здесь

<ul>
@for (item of fruits | combine: vegetables; track item) {
  <li>{{ item }}</li>
}
</ul>

3️⃣ Реально сложные вычисления

Сомнительный кейс с точки зрения юнит-тестирования. Но если у вас есть сервис, который маппит DTO в модель, то почему бы не отрефакторить его с использованием PipeTransform?

<ul>
@for (item of apiResponse | innerModels; track item) {
  <li>{{ item }}</li>
}
</ul>
@Pipe({ name: 'innerModels' })
export class InnerModelPipe implements PipeTransform {
  transform(value: ResponseDto): InnerModels {
    // some transformations here ...
  }
}

? Заключение

Поначалу Pipes могут выглядеть неудобными и бесполезными. Но при большем погружении в Angular они начинают казаться всё более привлекательными. В то же время ни в коем случае не призываю искушаться и обмазывать всю кодовую базу пайпами:

  • ❌ Обилие преобразований в шаблоне усложняет написание юнит-тестов

  • ❌ Создание своих Pipes требует бойлерплейта

А вот здоровое использование своей / сторонней библиотеки пайпов-утилит я бы точно порекомендовал. Это может существенно облегчить вашу жизнь и улучшить производительность приложения. Я бы руководствовался следующим небольшим чеклистом в вопросе переноса функционала в Pipes:

  • ✔️ Ресурсоемкая операция, особенно если она выделяет память

  • ✔️ Имеет потенциал для переиспользования (или можно свести до какого-то переиспользуемого примитива, в духе slice или split)

P.S. Надеюсь, моя статья не продублировала уже существующую на Хабре. Беглым поиском нашел только англоязычную статью

Комментарии (3)


  1. Ashita
    12.11.2025 07:52

    Сейчас изучаю angular на одном курсе и там как раз очень не хватало темы pipe и ваша статья отлично закрывает этот пробел в знаниях. Спасибо вам :) (P.s отсутствие упоминания asynpipe и jsonpipe удручает)


    1. eeeeeeeeeeee Автор
      12.11.2025 07:52

      Спасибо за комментарий. Рад, что статья оказалась полезной )

      Отсутствие AsyncPipe - конечно, упущение. А JsonPipe, как правило, на текущем проекте мы не используем - поэтому я и не подумал о нём. Либо через дебаггер смотрим, либо через расширение Redux DevTools (используем NGXS)


  1. lrmpsm53
    12.11.2025 07:52

    Пайпы нужны для маппинга данных в шаблоне