Оптимизация обнаружения изменений Angular

Внедрите более быстрое обнаружение изменений для лучшего взаимодействия с пользователем.

Angular периодически запускает свой механизм обнаружения изменений , чтобы изменения в модели данных отражались в представлении приложения. Обнаружение изменений может быть запущено вручную или через асинхронное событие (например, взаимодействие с пользователем или завершение XHR).

Обнаружение изменений — мощный инструмент, но если он запускается очень часто, он может вызвать множество вычислений и заблокировать основной поток браузера.

В этом посте вы узнаете, как контролировать и оптимизировать механизм обнаружения изменений, пропуская части вашего приложения и запуская обнаружение изменений только при необходимости.

Внутри обнаружения изменений Angular

Чтобы понять, как работает обнаружение изменений в Angular, давайте посмотрим на пример приложения!

Код приложения вы можете найти в этом репозитории GitHub .

В приложении перечислены сотрудники двух отделов компании — продаж и отдела исследований и разработок — и оно состоит из двух компонентов:

  • AppComponent , который является корневым компонентом приложения, и
  • Два экземпляра EmployeeListComponent : один для продаж и один для исследований и разработок.

Образец заявления

Вы можете увидеть два экземпляра EmployeeListComponent в шаблоне AppComponent :

<app-employee-list
  [data]="salesList"
  department="Sales"
  (add)="add(salesList, $event)"
  (remove)="remove(salesList, $event)"
></app-employee-list>

<app-employee-list
  [data]="rndList"
  department="R&D"
  (add)="add(rndList, $event)"
  (remove)="remove(rndList, $event)"
></app-employee-list>

Для каждого сотрудника есть имя и числовое значение. Приложение передает числовое значение сотрудника в бизнес-расчет и визуализирует результат на экране.

Теперь взгляните на EmployeeListComponent :

const fibonacci = (num: number): number => {
  if (num === 1 || num === 2) {
    return 1;
  }
  return fibonacci(num - 1) + fibonacci(num - 2);
};

@Component(...)
export class EmployeeListComponent {
  @Input() data: EmployeeData[];
  @Input() department: string;
  @Output() remove = new EventEmitter<EmployeeData>();
  @Output() add = new EventEmitter<string>();

  label: string;

  handleKey(event: any) {
    if (event.keyCode === 13) {
      this.add.emit(this.label);
      this.label = '';
    }
  }

  calculate(num: number) {
    return fibonacci(num);
  }
}

EmployeeListComponent принимает в качестве входных данных список сотрудников и название отдела. Когда пользователь пытается удалить или добавить сотрудника, компонент инициирует соответствующий вывод. Компонент также определяет метод calculate , который реализует бизнес-расчет.

Вот шаблон для EmployeeListComponent :

<h1 title="Department">{{ department }}</h1>
<mat-form-field>
  <input placeholder="Enter name here" matInput type="text" [(ngModel)]="label" (keydown)="handleKey($event)">
</mat-form-field>
<mat-list>
  <mat-list-item *ngFor="let item of data">
    <h3 matLine title="Name">
      {{ item.label }}
    </h3>
    <md-chip title="Score" class="mat-chip mat-primary mat-chip-selected" color="primary" selected="true">
      {{ calculate(item.num) }}
    </md-chip>
  </mat-list-item>
</mat-list>

Этот код перебирает всех сотрудников в списке и для каждого отображает элемент списка. Он также включает директиву ngModel для двусторонней привязки данных между входными данными и свойством label , объявленным EmployeeListComponent .

С помощью двух экземпляров EmployeeListComponent приложение формирует следующее дерево компонентов:

Дерево компонентов

AppComponent — это корневой компонент приложения. Его дочерние компоненты — это два экземпляра EmployeeListComponent . Каждый экземпляр имеет список элементов (E 1 , E 2 и т. д.), которые представляют отдельных сотрудников отдела.

Когда пользователь начинает вводить имя нового сотрудника в поле ввода в EmployeeListComponent , Angular запускает обнаружение изменений для всего дерева компонентов, начиная с AppComponent . Это означает, что пока пользователь вводит текст, Angular неоднократно пересчитывает числовые значения, связанные с каждым сотрудником, чтобы убедиться, что они не изменились с момента последней проверки.

Чтобы увидеть, насколько медленным это может быть, откройте неоптимизированную версию проекта на StackBlitz и попробуйте ввести имя сотрудника.

Вы можете убедиться, что замедление происходит из-за функции fibonacci настроив пример проекта и открыв вкладку «Производительность» в Chrome DevTools.

  1. Нажмите «Control+Shift+J» (или «Command+Option+J» на Mac), чтобы открыть DevTools.
  2. Откройте вкладку «Производительность» .

Теперь нажмите Запись (в верхнем левом углу панели «Производительность ») и начните вводить текст в одно из текстовых полей приложения. Через несколько секунд нажмите «Записать». еще раз, чтобы остановить запись. Как только Chrome DevTools обработает все собранные данные профилирования, вы увидите что-то вроде этого:

Профилирование производительности

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

Пропуск поддеревьев компонентов

Когда пользователь вводит текстовый ввод для компонента Sales EmployeeListComponent вы знаете, что данные в отделе исследований и разработок не меняются, поэтому нет смысла запускать обнаружение изменений в его компоненте. Чтобы убедиться, что экземпляр R&D не инициирует обнаружение изменений, установите changeDetectionStrategy в EmployeeListComponent значение OnPush :

import { ChangeDetectionStrategy, ... } from '@angular/core';

@Component({
  selector: 'app-employee-list',
  template: `...`,
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrls: ['employee-list.component.css']
})
export class EmployeeListComponent {...}

Теперь, когда пользователь вводит текст, обнаружение изменений срабатывает только для соответствующего отдела:

Обнаружение изменений в поддереве компонента

Эту оптимизацию в применении к оригинальному приложению вы можете найти здесь .

Подробнее о стратегии обнаружения изменений OnPush можно прочитать в официальной документации Angular .

Чтобы увидеть эффект от такой оптимизации, введите нового сотрудника в приложение на StackBlitz .

Использование чистых труб

Несмотря на то, что стратегия обнаружения изменений для EmployeeListComponent теперь установлена ​​на OnPush , Angular по-прежнему пересчитывает числовое значение для всех сотрудников в отделе, когда пользователь вводит соответствующий текстовый ввод.

Чтобы улучшить это поведение, вы можете воспользоваться чистыми каналами . Как чистые, так и нечистые каналы принимают входные данные и возвращают результаты, которые можно использовать в шаблоне. Разница между ними заключается в том, что чистый канал пересчитывает свой результат только в том случае, если он получает входные данные, отличные от предыдущего вызова.

Помните, что приложение вычисляет значение для отображения на основе числового значения сотрудника, вызывая метод calculate определенный EmployeeListComponent . Если вы переместите вычисление в чистый канал, Angular пересчитает выражение канала только при изменении его аргументов. Платформа определит, изменились ли аргументы канала, выполнив проверку ссылок. Это означает, что Angular не будет выполнять никаких перерасчетов, пока числовое значение сотрудника не будет обновлено.

Вот как можно переместить бизнес-расчет в канал CalculatePipe :

import { Pipe, PipeTransform } from '@angular/core';

const fibonacci = (num: number): number => {
  if (num === 1 || num === 2) {
    return 1;
  }
  return fibonacci(num - 1) + fibonacci(num - 2);
};

@Pipe({
  name: 'calculate'
})
export class CalculatePipe implements PipeTransform {
  transform(val: number) {
    return fibonacci(val);
  }
}

Метод transform канала вызывает функцию fibonacci . Обратите внимание, что труба чистая. Angular будет считать все каналы чистыми, если вы не укажете иное.

Наконец, обновите выражение внутри шаблона EmployeeListComponent :

<mat-chip-list>
  <md-chip>
    {{ item.num | calculate }}
  </md-chip>
</mat-chip-list>

Вот и все! Теперь, когда пользователь вводит текст, связанный с каким-либо отделом, приложение не будет пересчитывать числовое значение для отдельных сотрудников.

В приложении ниже вы можете увидеть, насколько плавнее становится набор текста!

Чтобы увидеть эффект последней оптимизации, попробуйте этот пример на StackBlitz .

Код с чистой конвейерной оптимизацией оригинального приложения доступен здесь .

Заключение

Если вы столкнулись с замедлением выполнения приложения Angular:

  1. Профилируйте приложение с помощью Chrome DevTools, чтобы увидеть причины замедления.
  2. Внедрите стратегию обнаружения изменений OnPush для сокращения поддеревьев компонента.
  3. Переместите тяжелые вычисления в чистые каналы, чтобы позволить платформе выполнять кэширование вычисленных значений.
,

Внедрите более быстрое обнаружение изменений для лучшего взаимодействия с пользователем.

Angular периодически запускает свой механизм обнаружения изменений , чтобы изменения в модели данных отражались в представлении приложения. Обнаружение изменений может быть запущено вручную или через асинхронное событие (например, взаимодействие с пользователем или завершение XHR).

Обнаружение изменений — мощный инструмент, но если он запускается очень часто, он может вызвать множество вычислений и заблокировать основной поток браузера.

В этом посте вы узнаете, как контролировать и оптимизировать механизм обнаружения изменений, пропуская части вашего приложения и запуская обнаружение изменений только при необходимости.

Внутри обнаружения изменений Angular

Чтобы понять, как работает обнаружение изменений в Angular, давайте посмотрим на пример приложения!

Код приложения вы можете найти в этом репозитории GitHub .

В приложении перечислены сотрудники двух отделов компании — продаж и отдела исследований и разработок — и оно состоит из двух компонентов:

  • AppComponent , который является корневым компонентом приложения, и
  • Два экземпляра EmployeeListComponent : один для продаж и один для исследований и разработок.

Образец заявления

Вы можете увидеть два экземпляра EmployeeListComponent в шаблоне AppComponent :

<app-employee-list
  [data]="salesList"
  department="Sales"
  (add)="add(salesList, $event)"
  (remove)="remove(salesList, $event)"
></app-employee-list>

<app-employee-list
  [data]="rndList"
  department="R&D"
  (add)="add(rndList, $event)"
  (remove)="remove(rndList, $event)"
></app-employee-list>

Для каждого сотрудника есть имя и числовое значение. Приложение передает числовое значение сотрудника в бизнес-расчет и визуализирует результат на экране.

Теперь взгляните на EmployeeListComponent :

const fibonacci = (num: number): number => {
  if (num === 1 || num === 2) {
    return 1;
  }
  return fibonacci(num - 1) + fibonacci(num - 2);
};

@Component(...)
export class EmployeeListComponent {
  @Input() data: EmployeeData[];
  @Input() department: string;
  @Output() remove = new EventEmitter<EmployeeData>();
  @Output() add = new EventEmitter<string>();

  label: string;

  handleKey(event: any) {
    if (event.keyCode === 13) {
      this.add.emit(this.label);
      this.label = '';
    }
  }

  calculate(num: number) {
    return fibonacci(num);
  }
}

EmployeeListComponent принимает в качестве входных данных список сотрудников и название отдела. Когда пользователь пытается удалить или добавить сотрудника, компонент инициирует соответствующий вывод. Компонент также определяет метод calculate , который реализует бизнес-расчет.

Вот шаблон для EmployeeListComponent :

<h1 title="Department">{{ department }}</h1>
<mat-form-field>
  <input placeholder="Enter name here" matInput type="text" [(ngModel)]="label" (keydown)="handleKey($event)">
</mat-form-field>
<mat-list>
  <mat-list-item *ngFor="let item of data">
    <h3 matLine title="Name">
      {{ item.label }}
    </h3>
    <md-chip title="Score" class="mat-chip mat-primary mat-chip-selected" color="primary" selected="true">
      {{ calculate(item.num) }}
    </md-chip>
  </mat-list-item>
</mat-list>

Этот код перебирает всех сотрудников в списке и для каждого отображает элемент списка. Он также включает директиву ngModel для двусторонней привязки данных между входными данными и свойством label , объявленным EmployeeListComponent .

С помощью двух экземпляров EmployeeListComponent приложение формирует следующее дерево компонентов:

Дерево компонентов

AppComponent — это корневой компонент приложения. Его дочерние компоненты — это два экземпляра EmployeeListComponent . Каждый экземпляр имеет список элементов (E 1 , E 2 и т. д.), которые представляют отдельных сотрудников отдела.

Когда пользователь начинает вводить имя нового сотрудника в поле ввода в EmployeeListComponent , Angular запускает обнаружение изменений для всего дерева компонентов, начиная с AppComponent . Это означает, что пока пользователь вводит текст, Angular неоднократно пересчитывает числовые значения, связанные с каждым сотрудником, чтобы убедиться, что они не изменились с момента последней проверки.

Чтобы увидеть, насколько медленным это может быть, откройте неоптимизированную версию проекта на StackBlitz и попробуйте ввести имя сотрудника.

Вы можете убедиться, что замедление происходит из-за функции fibonacci настроив пример проекта и открыв вкладку «Производительность» в Chrome DevTools.

  1. Нажмите «Control+Shift+J» (или «Command+Option+J» на Mac), чтобы открыть DevTools.
  2. Откройте вкладку «Производительность» .

Теперь нажмите Запись (в верхнем левом углу панели «Производительность ») и начните вводить текст в одно из текстовых полей приложения. Через несколько секунд нажмите «Записать». еще раз, чтобы остановить запись. Как только Chrome DevTools обработает все собранные данные профилирования, вы увидите что-то вроде этого:

Профилирование производительности

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

Пропуск поддеревьев компонентов

Когда пользователь вводит текстовый ввод для компонента Sales EmployeeListComponent вы знаете, что данные в отделе исследований и разработок не меняются, поэтому нет смысла запускать обнаружение изменений в его компоненте. Чтобы убедиться, что экземпляр R&D не инициирует обнаружение изменений, установите changeDetectionStrategy в EmployeeListComponent значение OnPush :

import { ChangeDetectionStrategy, ... } from '@angular/core';

@Component({
  selector: 'app-employee-list',
  template: `...`,
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrls: ['employee-list.component.css']
})
export class EmployeeListComponent {...}

Теперь, когда пользователь вводит текст, обнаружение изменений срабатывает только для соответствующего отдела:

Обнаружение изменений в поддереве компонента

Эту оптимизацию в применении к оригинальному приложению вы можете найти здесь .

Подробнее о стратегии обнаружения изменений OnPush можно прочитать в официальной документации Angular .

Чтобы увидеть эффект от такой оптимизации, введите нового сотрудника в приложение на StackBlitz .

Использование чистых труб

Несмотря на то, что стратегия обнаружения изменений для EmployeeListComponent теперь установлена ​​на OnPush , Angular по-прежнему пересчитывает числовое значение для всех сотрудников в отделе, когда пользователь вводит соответствующий текстовый ввод.

Чтобы улучшить это поведение, вы можете воспользоваться чистыми каналами . Как чистые, так и нечистые каналы принимают входные данные и возвращают результаты, которые можно использовать в шаблоне. Разница между ними заключается в том, что чистый канал пересчитывает свой результат только в том случае, если он получает входные данные, отличные от предыдущего вызова.

Помните, что приложение вычисляет значение для отображения на основе числового значения сотрудника, вызывая метод calculate определенный EmployeeListComponent . Если вы переместите вычисление в чистый канал, Angular пересчитает выражение канала только при изменении его аргументов. Платформа определит, изменились ли аргументы канала, выполнив проверку ссылок. Это означает, что Angular не будет выполнять никаких перерасчетов, пока числовое значение сотрудника не будет обновлено.

Вот как можно переместить бизнес-расчет в канал CalculatePipe :

import { Pipe, PipeTransform } from '@angular/core';

const fibonacci = (num: number): number => {
  if (num === 1 || num === 2) {
    return 1;
  }
  return fibonacci(num - 1) + fibonacci(num - 2);
};

@Pipe({
  name: 'calculate'
})
export class CalculatePipe implements PipeTransform {
  transform(val: number) {
    return fibonacci(val);
  }
}

Метод transform канала вызывает функцию fibonacci . Обратите внимание, что труба чистая. Angular будет считать все каналы чистыми, если вы не укажете иное.

Наконец, обновите выражение внутри шаблона EmployeeListComponent :

<mat-chip-list>
  <md-chip>
    {{ item.num | calculate }}
  </md-chip>
</mat-chip-list>

Вот и все! Теперь, когда пользователь вводит текст, связанный с каким-либо отделом, приложение не будет пересчитывать числовое значение для отдельных сотрудников.

В приложении ниже вы можете увидеть, насколько плавнее становится набор текста!

Чтобы увидеть эффект последней оптимизации, попробуйте этот пример на StackBlitz .

Код с чистой конвейерной оптимизацией оригинального приложения доступен здесь .

Заключение

Если вы столкнулись с замедлением выполнения приложения Angular:

  1. Профилируйте приложение с помощью Chrome DevTools, чтобы увидеть причины замедления.
  2. Внедрите стратегию обнаружения изменений OnPush для сокращения поддеревьев компонента.
  3. Переместите тяжелые вычисления в чистые каналы, чтобы позволить платформе выполнять кэширование вычисленных значений.