Производительность Angular: оптимизация шаблонных выражений
Выражения Template были для меня одной из самых полезных функций, когда я начал использовать Angular в 2017 году. Определите ваши данные / свойство в компоненте и отобразите его в компоненте с помощью двойных фигурных скобок - это было потрясающе. Хотя, если это показалось мне очень полезным, мне все равно нужно было узнать лучшие практики использования шаблонных выражений в Angular.
В этой статье мы рассмотрим выражения шаблонов в Angular и то, как их оптимизировать, чтобы улучшить производительность наших приложений.
Совет: используйте Bit (GitHub) для ваших Angular компонентов. Это поможет вам организовать, обмениваться и повторно использовать их между приложениями, чтобы быстрее создавать приложения. Также интересно обнаружить ваши компоненты в визуальной коллекции, попробовать игровую площадку и установить код.
Выражения в шаблонах
Давайте начнем с примера:
@Component({ selector: 'app-chat', template: `{{viewRun}}`, style: [] }) export class ChatComponent implements OnInit /*, DoCheck, OnChanges*/ { count = 0 onClick(evt) { this.count++ } get viewRun() { console.log('view updated:', this.count) return true } }Count Component {{count}}
У нас есть две привязки в нашем шаблоне: viewRun
и count
. Это говорит Angular, что поле в шаблоне является динамическим, то есть оно будет динамически обновляться во время запуска CD Angular.
count
является свойством a viewRun
методом ChatComponent. Когда Cd запускается на ChatComponent, Angular получает текущее значение свойства count и заменяет его на {{count}}
в шаблоне. viewRun
будучи функцией, выполняется и возвращаемое значение заменяет в шаблоне выражение {{viewRun}}
.
Вы видите, почему они называются привязками. Они склеивают свойства / методы компонента класса с представлением. При каждом нажатии кнопки Click Me
, свойство count увеличивается на 1. Теперь приращение обновляется в представлении {{count}}
при каждом запуске CD.
Запуск CD на Angular запускается:
- Асинхронные события:
- События DOM, например, клик, изменения, наведение мыши и т.д.
- setTimeout, setInterval и т.д.
- Неявный вызов CD компонентом.
Узкое место в производительности в выражениях шаблонов
Всякий раз, когда CD запускается на ChatComponent, привязки запускаются и обновляются. Привязки в компоненте, особенно в функции viewRun, вызываются, и результат отображается в DOM во время интерполяции DOM.
На самом деле мы вызываем функцию в шаблоне компонента ChatComponent. Что произойдет, если функция 'viewRun' не завершится быстро? Пользователи будут испытывать дергание или замедление.
В демонстрационных целях давайте заставим функцию 'viewRun' выполняться очень медленно, например, несколько секунд до ее завершения.
function viewRun() { function wait(ms) { let now = Date.now() let end = now + ms while (end > now) { now = Date.now() } } wait(20000) }
Видите, мы запустили viewRun за 20 секунд! Это огромное снижение производительности. Таким образом, пользователям придется подождать более 20 секунд, чтобы увидеть что-то на экране.
Лучшие практики в Angular предупреждают нас не вычислять тяжелые операции в шаблоне, мы должны выполнять только те функции, которые завершаются немедленно.
Оптимизировать выражения шаблона
Давайте посмотрим на другой пример:
function factorial(num: number) { if (num == 1) return 1 return num * factorial(num - 1) } @Component({ selector: 'app-users', template: `{{user.name}} {{user.id}} Factorial of User ID {{factorial(user.id)}}`, changeDetection: ChangeDetectionStrategy.OnPush }) class Users { @Input() users: Array= [] } @Component({ selector: 'app', template: ` ` }) class App { users: Array = [ { id: 99, name: "Nnamdi" }, { id: 88, name: "David" }, { id: 77, name: "Philip" }, { id: 66, name: "Chidume" } ] lastId = 66 addToUsers() { this.users = this.users.concat([ { id: this.lastId++, name: "User #ID: " + this.lastId } ]) } } interface User { id: number; name: string; }
У нас есть два компонента, Users и компоненты App. В компоненте Users он перечисляет данные пользователей, хранящиеся в массиве, используя директиву *ngFor
. Он отображает имя, идентификатор и факторный номер идентификатора пользователя. Компонент Users - это OnPush
компонент, это «чистый» компонент в том смысле, что Angular обновляет компонент при изменении входных привязок в компоненте. Когда мы нажимаем кнопку Add A User
, новый пользователь добавляется в массив users
, это вызывает повторный рендеринг компонента Users для отображения нового пользователя.
Теперь для каждого пользователя, добавляемого в массив users
, расчитывается факториальное шаблонное выражение для всех пользователей. Представьте, что в нашем массиве около 1000 пользователей, отображаемых на экране, и мы щелкнули, чтобы добавить в массив другого пользователя. Функция factorial будет запущена для 1001 пользователя!!! Мы все знаем, что вычисление факториала чисел может сильно загружать процессор.
Хотя мы оптимизировали наш компонент, чтобы не запускать ненужные триггеры CD, у нас есть проблемы в выражениях шаблонов. Да, мы не можем отказаться от функции factorial, но мы найдем способ заставить выполняться ее меньше при добавлении нового пользователя в массив.
Мы будем использовать Caching / Memoization, чтобы наша функция факториала выполнялась реже. Что это значит?
Наша функция factorial чистая и не имеет побочных эффектов, вы передаете аргумент, она возвращает результат на основе переданных аргументов. Мы можем запомнить его, заставив хранить результаты каждого ввода и возвращаться при последующих вызовах, когда функция факториала вызывается с теми же аргументами.
Пример:
Чтобы найти факториал числа, его умножают на его разность.
2! = 2 * 1 4! = 4 * 3 * 2 * 1 6! = 6 * 5 * 4 * 3 * 2 * 1 10! = 10 * 9 * 8 * 7 * 6 * 5 * 4 * 3 * 2 * 1
Посмотри на 4! = 4 * 3 * 2 * 1, знаете ли вы, что мы можем рассчитать только для 4 * 3 и вернуть его предыдущий расчет 2!. Вместо того, чтобы вычислять все заново, тогда как мы уже сделали это ранее.
4! = 4 * 3 * 2!
Это запоминание, вместо того, чтобы заново вычислять новое значение, мы просто ищем его, чтобы узнать, было ли оно уже вычислено ранее, и получаем результат оттуда.
Изменим функцию factorial:
function factorial(num) { if (!factorial.cache) { factorial.cache = {} } if (!factorial.cache[num]) { if (num == 1 || num == 0) return 1 factorial.cache[num] = num * factorial(num - 1) return factorial.cache } return factorial.cache[num] }
Вот это да!! много кода. Все, что мы здесь сделали, это просто посмотрим на число в объекте кеша factorial.cache
, если он присутствует, возвращается результат, если нет, выполняется поиск факториала, а результат добавляется в кеш и возвращается.
Давайте проверим это.
// ... function factorial(num) { if (!factorial.cache) { factorial.cache = {} } if (!factorial.cache[num]) { c.log("cache miss: ", num) if (num == 1 || num == 0) return 1 factorial.cache[num] = num * factorial(num - 1) return factorial.cache } c.log("cache hit: ", num) return factorial.cache[num] } c.log("2! call") factorial(2) c.log("\n") c.log("4! call") factorial(4) Admin@PHILIPSZDAVIDO MINGW64 /c/wamp/www/developerse/projects/trash $ node csl 2! call cache miss: 2 cache miss: 1 4! call cache miss: 4 cache miss: 3 cache hit: 2
Отлично!! как мы и ожидали. Если мы применим это к нашему компоненту Users, мы значительно минимизируем частоту глубоких вычислений, не запутывая их, функция factorial всегда будет вызываться при изменении входных привязок и срабатывании CD, мы просто оптимизировали его, чтобы не делать ненужные вычисления, которые он ранее уже делал.
function factorial(num: number) { if (!factorial.cache) { factorial.cache = {} } if (!factorial.cache[num]) { c.log("cache miss: ", num) if (num == 1 || num == 0) return 1 factorial.cache[num] = num * factorial(num - 1) return factorial.cache } c.log("cache hit: ", num) return factorial.cache[num] } @Component({ selector: 'app-users', template: `{{user.name}} {{user.id}} Factorial of User ID {{factorial(user.id)}}`, changeDetection: ChangeDetectionStrategy.OnPush }) class Users { @Input() users: Array= [] } // ...
Используем чистые Pipe
Мы можем сделать то же самое, используя чистые Pipe. Что такое чистые Pipe? Чистые каналы возвращают одинаковые выходные данные для определенных входных данных, они не имеют побочных эффектов для программы. Чистые Pipe оценивают данное выражение, только если они давали другой набор входных данных от в предыдущее их использование.
Примерами чистых Pipe в Angular являются Decimal pipe, Number pipe, Percent pipe, Currency pipe и Date pipe.
Эти pipe чисты, потому что:
- У них нет побочных эффектов
- Они возвращают определенный вывод для конкретного ввода.
Возьмем Date pipe в качестве примера
{{ 10 June 1985 | date: 'shortDate' }} 6/10/85
Теперь, когда мы в первый раз его вызвали 10 June 1985
, Angular оценит выражение, но при последующих вызовах с тем же вводом выражение будет оцениваться не ранее, а в prev. значение будет возвращено, потому что аргументы не изменились.
Давайте сделаем нашу факториальную функцию чистым pipe:
function factorial(num: number) { if (num == 1) return 1 return num * factorial(num - 1) } @Pipe({ name: 'factorial', pure: true }) class Factorial { transform(val: number) { return factorial(val) } }
Видите, мы переместили некэшированную версию нашей факториальной функции в чистый канал, нам больше не понадобится наша кэшированная версия, которую Angular предоставит нам!!
Теперь мы можем удалить выражение шаблона {{ factorial(user.id) }}
и заменить его на {{ user.id | factorial }}
.
Использование декораторов
Декоратор - это новая функция в TypeScript, добавленная для добавления дополнительной информации к классу, методу, свойству во время выполнения. Обозначается с помощью символа @
.
Angular сделал интенсивное использование декораторов:
- @Component
- @Directive
- @Output
- @Input
- @Optional
- @Self
- @Injectable
- @NgModule
- @Pipe
- …и так далее
Мы можем использовать декоратор для добавления механизмов кэширования в наше приложение.
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)) } } function purify () { return function(target: any, key: any, descriptor: any) { const oldFunc = descriptor.value const newFunc = memoize(oldFunc) descriptor.value=function() { return newFunc.apply(this,arguments) } } }
У нас есть наш метод декоратора purify
, он возвращает функцию, которая принимает три параметра: target
класс метода, который будет декорирован; key
название метода; descriptor
объект, содержащий функцию / метод в свойстве value.
В реализации мы сохраняем исходную функцию в переменной oldFunc, затем запоминаем ее, передавая oldFunc функции memoize
. Функция memoize
является общей функцией , используемой для memoize функций. Возвращенная запомненная функция сохраняется в переменной newFunc. Затем мы присваиваем новую функцию свойству descriptor.value
, реализация функции вызывает newFunc (запомненную версию) и возвращает значение.
Чтобы проверить это, мы создадим фиктивный класс с помощью факториального метода и украсим его декоратором purify.
class PureClass { @purify() factorial(num: number): number { console.log("cache miss: ", num) if (num == 1) return 1 return num * this.factorial(num - 1) } }
Смотрите, мы украсили факториальный метод с @purify()
. Давайте запустим это:
const p = new PureClass() console.log(p.factorial(2)) console.log(p.factorial(4)) Admin@PHILIPSZDAVIDO MINGW64 /c/wamp/www/developerse/projects/trash/di-ts $ ts-node mem cache miss: 2 cache miss: 1 2 cache miss: 4 cache miss: 3 24
Видите, у нас есть кэш-промах на 2, потому что это первый раз, когда он вызывается в следующий раз, когда он будет возвращен из кеша. Для 4 мы имеем промах кэша для 4 и 3 и хит для 2, потому что 2 были кэшированы ранее. Если мы вызовем 6, у нас будет попадание в кэш для 4,3,2, но не для 6 и 5, потому что это первый раз, когда функция видит это.
Теперь давайте применим его к нашему компоненту класса Angular:
@Component({ selector: 'app-users', template: `{{user.name}} {{user.id}} Factorial of User ID {{factorial(user.id)}}`, changeDetection: ChangeDetectionStrategy.OnPush }) class Users { @Input() users: Array= [] @purify() factorial(num: number) { if (num == 1) return 1 return num * factorial(num - 1) } }
Заключение
Мы узнали, как оптимизировать выражения шаблонов в этом посте. Мы видели замедления, которые могут возникнуть, если мы запустим высокопроизводительную функцию в наших шаблонах. Кроме того, мы рассмотрели различные способы оптимизации выражений шаблонов путем кэширования функций:
- мемоизации
- чистыех pipe
- декораторов
В следующем цикле проверки производительности Angular мы рассмотрим, как оптимизировать директиву *ngFor
в нашем шаблоне, чтобы избежать ненужных манипуляций с DOM.
Перевод статьи: Angular Performance: Optimize Template Expressions