Angular — это мощный инструмент для создания сложных веб-приложений. Но, как и в любом другом фреймворке, возникают свои сложности. Одна из таких проблем — это частые перезапуски тяжелых функций в шаблонах, что сильно бьет по производительности. Если приложение начинает тормозить, значит пора задуматься об оптимизации. И здесь на помощь приходит Memoize Pipe, способный спасти ваш интерфейс от лишних вычислений.

Проблема: Повторные вызовы функций

Каждый раз, когда Angular проверяет изменения, он повторно вызывает функции в шаблоне, игнорируя предыдущий результат. Это может стать проблемой, когда дело касается сложных вычислений или обращений к API.

@Component({
  ...
  template: `
    {{ heavyComputation(person, index) }}
  `,
})
export class AppComponent {
  heavyComputation(name: string, index: number) {
    // Очень тяжелые вычисления
  }
}

Решение: Memoize Pipe для повышения производительности

Создавать отдельный pipe для каждой функции не всегда удобно, особенно если функция больше нигде не используется. Хочется универсального и гибкого решения, и именно это предлагает Memoize Pipe. Он позволяет запоминать результаты функций, избегая их повторного вызова, если входные данные остались неизменными. Это снижает нагрузку на приложение и улучшает производительность.

Пример использования:

Изначально у вас есть шаблон с вызовом функции:

@Component({
  ...
  template: `
    {{ heavyComputation(person, index) }}
  `,
})
export class AppComponent {
  heavyComputation(name: string, index: number) {
    // Очень тяжелые вычисления
  }
}

Теперь добавим мемоизацию:

Установим библиотеку:

npm i @ngx-rock/memoize-pipe

Затем импортируем FnPipe и используем его в шаблоне:

@Component({
  ...
  template: `
    {{ heavyComputation | fn : person : index }}
  `,
})
export class AppComponent {
  heavyComputation(name: string, index: number) {
    // Очень тяжелые вычисления
  }
}

Теперь Angular не будет каждый раз пересчитывать функцию, если значения её аргументов не изменились (примитивы и ссылки для массивов и объектов).

Сохранение контекста this

Если ваша функция зависит от переменных компонента, то нужно сохранить контекст. Это можно сделать с помощью стрелочной функции:

@Component(...)
export class AppComponent {     
  heavyComputation = (name: string, index: number) => {
    // Очень тяжелые вычисления
  }
}

Поддержка standalone модулей

Последние версии Memoize Pipe реализованы в виде standalone модулей.

import { FnPipe } from "@ngx-rock/memoize-pipe";

@Component({
  ...
  standalone: true,
  imports: [ FnPipe, ... ]
  ...
})
export class AppComponent { ... }

Заключение: Простая оптимизация — ощутимый результат

Когда речь идет о производительности, порой самые простые решения оказываются наиболее эффективными. Memoize Pipe — это как раз такое решение: оно помогает уменьшить количество вызовов функций, ускоряя работу приложения. Помимо этого упрощает процесс написания кода.

@ngx-rock/memoize-pipe

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


  1. jonic
    24.09.2024 16:39

    Все хорошо, только не проще ли тогда использовать computed сигнал? Мемоизация из коробки


    1. BaryshevRS Автор
      24.09.2024 16:39
      +1

      Тоже имеет право на жизнь, но не всегда применимо. Например, в цикле в шаблоне.

      Консоль лог вызывается снова, когда проверяются изменения в шаблоне.


      1. jonic
        24.09.2024 16:39
        +1

        Я бы в этом примере передал обработанный массив в цикл for. Как минимум не пришлось бы делать N вызовов метода внутри цикла. К тому же тут вообще ужасно создается каждый цикл новый сигнал. Когда они будут освобождены? Вообще на сколько помню сигналы можно создавать только внутри конструктора или контекста инъекции.


        1. BaryshevRS Автор
          24.09.2024 16:39

          Код выше у меня синтетический, но при этом работает. Это просто пример того, что computed сигнал не универсальное решение.

          На счёт предвычислений можно, конечно, но иногда есть ситуации, когда проще в шаблоне.


          1. jonic
            24.09.2024 16:39

            Я бы такое на ревью не пропустил. Все же, учитывая куда все движется, приспосабливаться к сигналам. Это поможет избежать ошибок “value was changed after checked” как минимум. Да я как ангулярщик со стажем (с самых первых версий) был немного скептичен к ним, но учитывая что у нас уже идут input output signals, то.. за ними будущее как и удаление zone.js :)


            1. BaryshevRS Автор
              24.09.2024 16:39

              Это всё хорошо, но “value was changed after checked” никак не появится в моём решении. И это никак не касается зоны. Такие перерасчеты есть и при её удалении.

              Input output signals пока всё же экспериментальные. И они не замена pipe. Такого позиционирования в документации не видел.


              1. jonic
                24.09.2024 16:39

                Нет, это не замена pipe, мне больше не нравится что функция вызывается в pipe трансформ и кэшируется для определенного значения + аргументов. Все же не экспериментальные, а уже developer preview.

                в вашем примере нет, а найдется умник который в тяжелом вычислении сделает смену состояния.

                Ну и извините, ради одного pipe который вызывает функцию в трансформ тащить зависимость?


                1. BaryshevRS Автор
                  24.09.2024 16:39

                  в вашем примере нет, а найдется умник который в тяжелом вычислении сделает смену состояния.

                  Вот это можно как раз на ревью увидеть.

                  Ну и извините, ради одного pipe который вызывает функцию в трансформ тащить зависимость?

                  Почему нет, уменьшает бордерплейт. Не нужно создавать лишние одноразовые пайпы, а сразу использовать внешние функции утилиты или методы прямо из компонента.


                  1. jonic
                    24.09.2024 16:39
                    +1

                    https://stackblitz.com/edit/stackblitz-starters-mbkbwg

                    Не знаю, на мой взгляд сигналы выразительнее и не надо ничего тащить или писать.

                    P.s. Обратите внимание, я заменил пакет целым файлом в 10 строк.