Как улучшить производительность вашего приложения Angular

Опубликовано: 2020-04-10

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

Одна из многих вещей, над которыми вам всегда нужно работать как разработчику внешнего интерфейса, — это производительность приложения. Большая часть моих прошлых проектов была сосредоточена на крупных корпоративных приложениях, которые продолжают расширяться и развиваться. Фронтенд-фреймворки здесь были бы крайне полезны, но важно использовать их правильно и разумно.

Я подготовил краткий список самых популярных стратегий повышения производительности и советов, которые могут помочь вам мгновенно повысить производительность вашего приложения Angular . Имейте в виду, что все приведенные здесь советы относятся к Angular версии 8.

ChangeDetectionStrategy и ChangeDetectorRef

Обнаружение изменений (CD) — это механизм Angular для обнаружения изменений данных и автоматического реагирования на них. Мы можем перечислить основные виды стандартных изменений состояния приложения:

  • События
  • HTTP-запрос
  • Таймеры

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

Ответ — ngZone , сложная система, предназначенная для отслеживания асинхронных взаимодействий. Если все операции регистрируются ngZone, Angular знает, когда реагировать на какие-то изменения. Но он не знает, что именно изменилось, и запускает механизм Change Detection , который проверяет все компоненты в порядке первой глубины.

У каждого компонента в приложении Angular есть свой Детектор изменений, который определяет, как этот компонент должен действовать при запуске Детектора изменений — например, если есть необходимость перерендерить DOM компонента (что является довольно дорогой операцией). Когда Angular запускает обнаружение изменений, каждый отдельный компонент будет проверен, и его представление (DOM) может быть перерисовано по умолчанию.

Мы можем избежать этого, используя ChangeDetectionStrategy.OnPush:

 @Составная часть({
  селектор: 'foobar',
  URL-адрес шаблона: './foobar.component.html',
  URL-адреса стилей: ['./foobar.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})

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

Стратегия сообщает Angular, что конкретный компонент зависит только от его @Inputs(). Кроме того, все компоненты @Inputs() будут действовать как неизменяемый объект (например, когда мы меняем только свойство в @Input() объекта, не меняя ссылку, этот компонент не будет проверяться). Это означает, что многие ненужные проверки будут опущены, и это должно повысить производительность нашего приложения.

Компонент с ChangeDetectionStrategy.OnPush будет проверяться только в следующих случаях:

  • Ссылка @Input() изменится
  • Событие будет запущено в шаблоне компонента или в одном из его дочерних элементов.
  • Наблюдаемый в компоненте вызовет событие
  • CD будет запускаться вручную с помощью сервиса ChangeDetectorRef.
  • асинхронный канал используется в представлении (асинхронный канал помечает компонент, который нужно проверить на наличие изменений — когда исходный поток выдаст новое значение, этот компонент будет проверен)

Если ничего из вышеперечисленного не происходит, использование ChangeDetectionStrategy.OnPush внутри определенного компонента приводит к тому, что компонент и все вложенные компоненты не будут проверяться после запуска компакт-диска.

К счастью, мы по-прежнему можем полностью контролировать реакцию на изменения данных с помощью службы ChangeDetectorRef. Мы должны помнить, что с ChangeDetectionStrategy.OnPush внутри наших обратных вызовов тайм-аутов, запросов, подписок нам нужно запускать CD вручную, если нам это действительно нужно:

 счетчик = 0;

конструктор (частный changeDetectorRef: ChangeDetectorRef) {}

нгонинит () {
  setTimeout(() => {
    этот.счетчик += 1000;
    this.changeDetectorRef.detectChanges();
  }, 1000);
}

Как мы можем видеть выше, вызывая this.changeDetectorRef.detectChanges() внутри нашей функции тайм -аута, мы можем принудительно включить CD вручную. Если счетчик каким-либо образом используется внутри шаблона, его значение будет обновлено.

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

 this.changeDetectorRef.detach()

Этот код должен выполняться внутри метода жизненного цикла ngAfterViewInit() или ngAfterViewChecked(), чтобы убедиться, что наше представление отобразилось правильно, прежде чем мы отключим обновление данных. Этот компонент больше не будет проверяться во время CD , если только мы не вызовем функцию detectChanges() вручную.

Вызовы функций и геттеры в шаблоне

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

Чистые трубы

Чистые трубы — это разновидность труб, выход которых зависит только от входа, без побочных эффектов. К счастью, все пайпы в Angular по умолчанию чистые.

 @Трубка({
    имя: 'верхний регистр',
    чистый: правда
})

Но почему мы должны избегать использования каналов с pure: false? Ответ снова Обнаружение изменений. Нечистые каналы выполняются при каждом запуске компакт-диска, что в большинстве случаев не требуется и снижает производительность нашего приложения. Вот пример функции, которую мы можем изменить на чистый канал:

 преобразование (значение: строка, предел = 60, многоточие = '...') {
  если (!значение || значение.длина <= лимит) {
    возвращаемое значение;
  }
  const numberOfVisibleCharacters = value.substr(0, limit).lastIndexOf(' ');
  вернуть `${value.substr(0, numberOfVisibleCharacters)}${многоточие}`;
}

И посмотрим вид:

 <p class="description">обрезать(текст, 30)</p>

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

 @Трубка({
  имя: 'усечение',
  чистый: правда
})
класс экспорта TruncatePipe реализует PipeTransform {
  преобразование (значение: строка, предел = 60, многоточие = '...') {
    ...
  }
}

И, наконец, в этом представлении мы получаем код, который будет выполняться только при изменении текста, независимо от Change Detection .

 <p class="description">{{ текст | обрезать: 30 }}</p>

Ленивая загрузка и предварительная загрузка модулей

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

 константные маршруты: Маршруты = [
  {
    дорожка: '',
    компонент: HomeComponent
  },
  {
    путь: 'фу',
    loadChildren: ()=> import("./foo/foo.module").then(m => m.FooModule)
  },
  {
    путь: 'бар',
    loadChildren: ()=> import("./bar/bar.module").then(m => m.BarModule)
  }
]
@NgModule({
  экспортирует: [RouterModule],
  импортирует: [RouterModule.forRoot(маршруты)]
})
класс AppRoutingModule {}

В приведенном выше примере мы видим, что fooModule со всеми его активами будет загружен только тогда, когда пользователь попытается ввести определенный маршрут (foo или bar). Angular также сгенерирует отдельный чанк для этого модуля . Ленивая загрузка уменьшит начальную загрузку .

Мы можем провести дополнительную оптимизацию. Предположим, что мы хотим, чтобы наше приложение загружало модули в фоновом режиме. В этом случае мы можем использовать preloadingStrategy. Angular по умолчанию имеет два типа preloadingStrategy:

  • Без предварительной загрузки
  • PreloadAllModules

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

 @NgModule({
  экспортирует: [RouterModule],
  импортирует: [RouterModule.forRoot(маршруты, {
       preloadingStrategy: PreloadAllModules
  }]
})
класс AppRoutingModule {}

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

Предположим, мы хотим предварительно загрузить только выбранные модули, например, BarModule. Мы указываем это, добавляя дополнительное поле для поля данных.

 константные маршруты: Маршруты = [
  {
    дорожка: '',
    компонент: HomeComponent
    данные: {предварительная загрузка: ложь}
  },
  {
    путь: 'фу',
    loadChildren: ()=> import("./foo/foo.module").then(m => m.FooModule),
    данные: {предварительная загрузка: ложь}
  },
  {
    путь: 'бар',
    loadChildren: ()=> import("./bar/bar.module").then(m => m.BarModule),
    данные: {предварительная загрузка: правда}
  }
]

Затем мы должны написать нашу пользовательскую функцию предварительной загрузки:

 @Инъекционный()
класс экспорта CustomPreloadingStrategy реализует PreloadingStrategy {
  preload(route: Route, load: () => Observable<any>): Observable<any> {
    вернуть route.data && route.data.preload ? загрузка (): из (ноль);
  }
}

И установите его как preloadingStrategy:

 @NgModule({
  экспортирует: [RouterModule],
  импортирует: [RouterModule.forRoot(маршруты, {
       preloadingStrategy: CustomPreloadingStrategy
  }]
})
класс AppRoutingModule {}

На данный момент будут предварительно загружены только маршруты с параметром { data: { preload: true } }. Остальные маршруты будут действовать так, как если бы был установлен параметр NoPreloading.

Custom preloadingStrategy — это @Injectable(), поэтому это означает, что мы можем внедрить некоторые сервисы внутрь, если нам нужно, и настроить нашу preloadingStrategy любым другим способом.
С помощью инструментов разработчика браузера мы можем исследовать повышение производительности за счет одинакового времени начальной загрузки с preloadingStrategy и без него. Мы также можем посмотреть на вкладку сети, чтобы увидеть, что фрагменты для других маршрутов загружаются в фоновом режиме, в то время как пользователь может видеть текущую страницу без каких-либо задержек.

функция trackBy

Мы можем предположить, что большинство приложений Angular используют *ngFor для перебора элементов, перечисленных внутри шаблона. Если повторяющийся список также доступен для редактирования, trackBy абсолютно необходим.

 <ул>
  <tr *ngFor="пусть продукт продуктов; trackBy: trackByProductId">
    <td>{{ product.title }}</td>
  </tr>
</ul>

trackByProductId (индекс: номер, продукт: продукт) {
  вернуть product.id;
}

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

Предварительная компиляция (AOT)

Что касается документации Angular:

« (…) компоненты и шаблоны, предоставляемые Angular, не могут быть поняты браузером напрямую, приложения Angular требуют процесса компиляции, прежде чем они смогут работать в браузере »

Angular предоставляет два типа компиляции:

  • Just-in-Time (JIT) — компилирует приложение в браузере во время выполнения.
  • Ahead-of-Time (AOT) — компилирует приложение во время сборки.

Для использования при разработке JIT- компиляция должна покрывать потребности разработчика. Тем не менее, для производственной сборки мы обязательно должны использовать AOT . Нам нужно убедиться, что для флага aot внутри файла angular.json установлено значение true. Наиболее важными преимуществами такого решения являются более быстрый рендеринг, меньшее количество асинхронных запросов, меньший размер загрузки фреймворка и повышенная безопасность.

Резюме

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

Выпуск значка продукта

Доверьте Miquido свой проект Angular

Свяжитесь с нами

Хотите разработать приложение с Miquido?

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