Шаблоны Angular потрясающие и чрезвычайно мощные.
Благодаря структурным директивам и привязкам свойств мы можем создавать даже самые сложные представления с очень четкой семантикой:
<ng-container *ngIf="isLoggedIn"> <h1>Welcome {{ fullName }}!</h1> </ng-container>
Поскольку выражения настолько мощны, они могут легко усложняться, когда наши взгляды становятся более сложными.
Прежде чем мы это узнаем, мы получаем вызовы функций в наших шаблонах Angular:
<ng-container *ngIf="isLoggedIn"> <h1>Welcome {{ fullName }}!</h1> <a href="files" *ngIf="hasAccessTo('files')">Files</a> <a href="photos" *ngIf="hasAccessTo('photos')">Photos</a> </ng-container>
Хотя вызовы функций в шаблонах Angular очень удобны и технически допустимы, они могут вызвать серьезные проблемы с производительностью.
В этой статье объясняется, почему и как можно обойти проблемы с производительностью.
Эта проблема
Для демонстрации предположим, что у нас есть PersonComponent
, который использует вызов функции fullName()
в своем шаблоне для отображения полного имени человека, которое передается через свойство person
:
@Component({ template: ` <p>Welcome {{ fullName() }}!</p> <button (click)="onClick()">Trigger change detection</button> ` }) export class PersonComponent { @Input() person: { firstName: string, lastName: string }; constructor() { } fullName() { return this.person.firstName + ' ' + this.person.lastName } onClick() { console.log('Button was clicked'; } }
Здесь функция fullName()
выполняется каждый раз, когда запускается обнаружение угловых изменений. И это может быть много раз!
Каждый раз, когда нажимается кнопка, выполняется fullName()
функция.
Хотя нажатие кнопки может показаться безобидным, предположим, что шесть месяцев спустя требования к нашему компоненту PersonComponent изменились, и теперь нам нужно иметь дело с большим количеством событий, запускающих обнаружение изменений:
@Component({ template: ` <p>Welcome {{ fullName() }}!</p> <div (mousemove)="onMouseMove()">Drop a picture here</div> ` }) export class PersonComponent { @Input() person: { firstName: string, lastName: string }; constructor() { } fullName() { return this.person.firstName + ' ' + this.person.lastName } onMouseMove() { console.log('Mouse was moved'); } }
Внезапно функция fullName()
выполняется сотни раз при наведении указателя мыши на div
.
И поскольку код fullName()
был написан уже шесть месяцев назад, мы можем не осознавать влияние нашего нового кода.
Кроме того, обнаружение изменений может быть запущено вне PersonComponent
:
<person [person]="person"></person> <button (click)="onClick()"> Trigger change detection outside of PersonComponent </button>
Здесь функция fullName()
внутри PersonComponent
выполняется каждый раз, когда нажимается кнопка за пределами PersonComponent
.
Так почему это происходит? И что мы можем с этим поделать?
Почему функции в выражениях вызываются так много раз?
Цель обнаружения изменений Angular - выяснить, какие части пользовательского интерфейса необходимо повторно отображать при возникновении изменений.
Чтобы определить, нужно ли перерисовывать <p>Welcome {{ fullName() }}!</p>
, Angular необходимо выполнить выражение fullName()
, чтобы проверить, изменилось ли его возвращаемое значение.
Поскольку Angular не может предсказать, изменилось ли возвращаемое значение fullName()
, ему необходимо выполнять функцию каждый раз, когда запускается обнаружение изменений.
Таким образом, если обнаружение изменений выполняется 300 раз, функция вызывается 300 раз, даже если ее возвращаемое значение никогда не изменяется.
В зависимости от логики внутри функции, запуск функции сотни раз может стать серьезной проблемой для производительности.
Когда используются геттеры, это становится болезненно скрытым в шаблонах:
@Component({ template: ` <p>Welcome {{ fullName }}!</p> ` }) export class PersonComponent { @Input() person: { firstName: string, lastName: string }; constructor() { } get fullName() { return this.person.firstName + ' ' + this.person.lastName } }
Здесь шаблон не показывает визуальной индикации выполнения какого-либо вызова функции, но геттер get fullName()
вызывается каждый раз, когда запускается обнаружение изменений.
А как насчет ChangeDetectionStrategy.OnPush?
Когда мы включаем ChangeDetectionStrategy.OnPush
для PersonComponent
, мы говорим обнаружению изменений Angular игнорировать изменения вне PersonComponent
, которые не влияют на его входные свойства.
В результате обнаружение изменений внутри PersonComponent
запускается только при изменении одного из его входных свойств, в данном случае его свойства person
.
В следующем примере:
<person [person]="person"></person> <button (click)="onClick()"> Trigger change detection outside of PersonComponent </button>
функция fullName()
внутри PersonComponent
больше не выполняется при нажатии кнопки за пределами PersonComponent
.
Блестяще! Разве это не решает всех наших потенциальных проблем с производительностью?
Нет, к сожалению нет.
Почему нет?
Поскольку функция fullName()
по-прежнему выполняется каждый раз, когда обнаружение изменений инициируется внутри самого PersonComponent
:
@Component({ template: ` <p>Welcome {{ fullName() }}!</p> <div (mousemove)="onMouseMove()">Drop a picture here</div> ` changeDetection: ChangeDetectionStrategy.OnPush }) export class PersonComponent { @Input() person: { firstName: string, lastName: string }; constructor() { } fullName() { return this.person.firstName + ' ' + this.person.lastName } onMouseMove() { console.log('Mouse was moved'); } }
Здесь функция fullName()
по-прежнему выполняется каждый раз, когда указатель мыши перемещается по div
, даже если у нас включено ChangeDetectionStrategy.OnPush
.
Таким образом, хотя ChangeDetectionStrategy.OnPush
может помочь нам игнорировать циклы обнаружения изменений извне, функции в наших выражениях по-прежнему выполняются каждый раз, когда обнаружение изменений запускается из самого компонента.
Итак, как мы можем предотвратить ненужные вызовы функций?
Решение: как избежать ненужных вызовов функций
Стратегия 1 - Чистые трубы
Первая стратегия уменьшения количества вызовов функций - использовать чистые каналы.
Сообщая Angular, что канал является чистым, Angular знает, что возвращаемое значение канала не меняется, если вход канала не изменяется.
В нашем существующем примере мы можем создать чистую трубу для вычисления полного имени человека:
import { Pipe, PipeTransform } from '@angular/core'; @Pipe({ name: 'fullName', pure: true }) export class FullNamePipe implements PipeTransform { transform(person: any, args?: any): any { return person.firstName + ' ' + person.lastName; } }
Канал в Angular по умолчанию является чистым, поэтому мы можем пропустить часть pure: true
в декораторе конвейера:
import { Pipe, PipeTransform } from '@angular/core'; @Pipe({ name: 'fullName' }) export class FullNamePipe implements PipeTransform { transform(person: any, args?: any): any { return person.firstName + ' ' + person.lastName; } }
Мы можем использовать канал, добавив его в качестве объявления в наш модуль:
@NgModule({ declarations: [ AppComponent, PersonComponent, FullNamePipe ] }) export class AppModule { }
и используя его с точки зрения нашего PersonComponent
:
@Component({ template: ` <p>Welcome {{ person | fullName }}!</p> <div (mousemove)="onMouseMove()">Drop a picture here</div> ` changeDetection: ChangeDetectionStrategy.OnPush }) export class PersonComponent implements OnChanges { @Input() person: { firstName: string, lastName: string }; constructor() { } onMouseMove() { console.log('Mouse was moved'); } }
Angular теперь достаточно умен, чтобы знать, что выражение {{ person | fullName }}
не изменится, если person
не изменится.
В результате Angular пропускает выполнение transform
метода канала, если человек не меняется.
Но что, если наша логика более сложна и не может быть обработана трубкой?
Стратегия 2. Расчет значений вручную
Вторая стратегия, позволяющая избежать ненужных вызовов функций, состоит в том, чтобы вручную вычислить значения, которые нам нужны в нашем представлении, из контроллера компонента.
Как разработчик, мы можем воспользоваться тем фактом, что знаем, когда значение выражения может измениться.
В нашем примере мы знаем, что полное имя может измениться только при изменении person
, поэтому мы можем добавить свойство fullName
к компоненту и пересчитать его значение в ngOnChanges
только при изменении значения входного person
компонента.
Это оставляет нам простое свойство, которое содержит необходимое нам значение, поэтому мы можем заменить выражение вызова функции {{ fullName() }}
в представлении выражением свойства {{ fullName }}
:
@Component({ template: ` <p>Welcome {{ fullName }}!</p> <div (mousemove)="onMouseMove()">Drop a picture here</div> ` changeDetection: ChangeDetectionStrategy.OnPush }) export class PersonComponent implements OnChanges { @Input() person: { firstName: string, lastName: string }; fullName = ''; constructor() { } ngOnChanges(changes: SimpleChanges) { if (changes.person) { this.fullName = this.calculateFullName(); } } calculateFullName() { return this.person.firstName + ' ' + this.person.lastName; } onMouseMove() { console.log('Mouse was moved'); } }
В результате свойство fullName
пересчитывается только при изменении входа person
компонента.
Когда мы наводим курсор на div
, обнаружение изменений больше не выполняет логику для вычисления полного имени.
Посмотрите, как это простое изменение сокращает 294 вызова функций до 5 вызовов функций, когда мы перемещаемся по объектам с 5 людьми и запускаем событие mousemove:
Визуально разницы нет. Но за кулисами влияние наших изменений огромно.
Вы можете сами поиграть с демоверсией прямо здесь: https://stackblitz.com/edit/angular-hxmb6q
Резюме
В этой статье мы узнали, что вызовы функций в выражении могут вызвать серьезные проблемы с производительностью, которые могут остаться незамеченными и замедлить работу вашего приложения даже при включенном ChangeDetectionStrategy.OnPush
.
Чтобы избежать проблем, настоятельно рекомендуется не использовать вызовы функций в выражениях шаблона Angular.
Вместо этого вы можете:
- используйте чистые каналы, чтобы Angular знал, что выполнение канала можно безопасно пропустить, если вход канала не изменяется.
- вычисляйте вручную нужные вам значения в контроллере вашего компонента и пересчитывайте их только при необходимости.
Чтобы увидеть влияние на производительность, вы можете поиграть с живым демо прямо здесь: https://stackblitz.com/edit/angular-hxmb6q
В следующий раз, когда вы обнаружите, что пишете вызов функции в шаблоне, обязательно подумайте о возможных последствиях.
И если вы заметили другого разработчика, использующего вызов функции в выражении шаблона Angular во время проверки кода, любезно отправьте ему ссылку на эту статью, чтобы поделиться с ним своими знаниями.
Потому что производительность имеет значение!
Всего хорошего!