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

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

Я обнаружил, что уже есть много руководств о том, как решить первую проблему, но не во второй. По моему опыту, одна из наиболее часто упускаемых из виду причин второй - это таймер, что означает setTimeout() и setInterval().

Проблема с setTimeout () и setInterval ()

В отличие от React или Vue, обнаружение изменений в Angular управляется событиями. Каждый раз, когда запускается setTimeout() или setInterval(), всегда срабатывает обнаружение изменений. Вот пример:

import { Component } from '@angular/core';
@Component({
  template: '{{ displayText }}'
})
export class Playground {
  data = ['a', 'b', 'c', 'd'];
  get displayText() {
    console.log('re-rendered');
    return this.data.join(', ');
  }
  ngOnInit() {
    setInterval(() => {
      console.log('Just did something...')
    }, 1000);
  }
}

Геттер displayText() используется для визуализации обнаружения изменений. Он всегда будет повторно визуализироваться всякий раз, когда будет обнаружено изменение, поэтому это означает, что обнаружение изменения происходит всякий раз, когда оно вызывается. Если вы откроете консоль, вы увидите эти журналы:

empty-playground.ts:11 re-rendered
empty-playground.ts:11 re-rendered
empty-playground.ts:17 Just did something...
empty-playground.ts:11 re-rendered
empty-playground.ts:11 re-rendered
empty-playground.ts:17 Just did something...
empty-playground.ts:11 re-rendered
empty-playground.ts:11 re-rendered
empty-playground.ts:17 Just did something...
empty-playground.ts:11 re-rendered
empty-playground.ts:11 re-rendered
...

Мы можем видеть, что даже если внутри setInterval() ничего не делается, обнаружение изменений все равно срабатывает.

Замечание №1: Геттер displayText() является потенциальной проблемой 1, упомянутой выше, поскольку его значение всегда пересчитывается, что приводит к повторному рендерингу. Тем не менее, вы, вероятно, сможете найти множество решений за пределами своей страны, поэтому в этой статье это обсуждаться не будет.

Замечание №2: текст 're-rendered' дважды выводится из системы. Это функция отладки при разработке Angular. Вот более подробное объяснение.

Решение 1: NgZone

NgZone - это встроенная служба Angular, позволяющая вручную исключить задачу из области обнаружения Angular. Проще говоря, сообщите Angular, когда запускать обнаружение изменений. Вам нужно знать два метода. Во-первых, используйте runOutsideAngular(), чтобы ничего внутри не инициировало обнаружение изменений. Во-вторых, используйте run(), чтобы сообщить Angular, что нужно запускать обнаружение изменений после выполнения внутренней функции.

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

import { Component, NgZone } from '@angular/core';
@Component({
  template: '{{ counter }} | {{ displayText }}'
})
export class Playground {
  counter = 0;
  data = ['a', 'b', 'c', 'd'];
  constructor(private _ngZone: NgZone) {}
  get displayText() {
    console.log('re-rendered');
    return this.data.join(', ');
  }
  ngOnInit() {
    this._ngZone.runOutsideAngular(() => {
      setInterval(() => {
        if (Math.random() < 0.1) {
          console.log('Should trigger a change detection');
          this._ngZone.run(() => { 
            this.counter++; 
          });
        }
        else {
          console.log('Should not trigger a change detection');
        }
      }, 1000);
    })
  }
}

Решение 2. Собственные методы + ChangeDetectorRef

Это альтернативное решение на тот случай, если вы хотите, чтобы ваш код оставался плоским, не оборачивая его NgZone методами. Вместо использования исправленного setInterval() Angular, который запускает обнаружение изменений, вы используете собственный setInterval(), который полностью выходит за рамки Angular. Вы используете ChangeDetectorRef, чтобы запускать обнаружение вручную, когда вам нужно.

import { Component, ChangeDetectorRef } from '@angular/core';
@Component({
  template: '{{ counter }} | {{ displayText }}'
})
export class Playground {
  counter = 0;
  data = ['a', 'b', 'c', 'd'];
  constructor(private _ref: ChangeDetectorRef) {}
  get displayText() {
    console.log('re-rendered');
    return this.data.join(', ');
  }
  ngOnInit() {
    const setInterval = window['__zone_symbol__setInterval'];
    setInterval(() => {
      if (Math.random() < 0.1) {
        console.log('Should trigger a change detection'); 
        this.counter++;
        this._ref.detectChanges();
      }
      else {
        console.log('Should not trigger a change detection');
      }
    }, 1000);
  }
}

Для двух наиболее распространенных проблем с производительностью, которые я упомянул в начале, наличие любой из них обычно не является большой проблемой. Поскольку в наши дни компьютер настолько быстр, вы, вероятно, ничего не почувствуете. Тем не менее, влияние мультипликативно. Он станет действительно большим, если в вашем приложении одновременно будут возникать эти две проблемы. Решить ненужный повторный рендеринг очень сложно. Может оказаться, что вам потребуется реализовать OnPush стратегию или даже detach() свои компоненты, что чрезвычайно болезненно для управления. Относительно проще решить проблему обнаружения ненужных изменений, поскольку исправление более изолировано. Никаких кардинальных изменений не произойдет.

Наконец, спасибо за чтение. Надеюсь, моя статья будет вам полезна!