От автора: сегодня мы поговорим о том, как оптимизировать производительность приложения на Angular с помощью ленивой загрузки, функций TrackBy, OnPush, отключения обнаружения изменений и других способов.
Использование OnPush
По умолчанию Angular запускает механизм определения изменений на всех компонентах каждый раз при изменении чего-либо в приложении – от события click до получения данных по ajax-запросу. (пользовательские события, таймеры, xhr, promise’ы и т.д.)
Представьте, например, что у нас есть компонент select.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
@Component({ selector: 'my-select', template: ` <select (change)="change()"> <option *ngFor="let option of options" [value]="option.id"> {{option.name}} </option> </select> ` }) export class MySelectComponent { @Input() options = []; change() {} } |
Мы собираемся передать массив навыков в компонент select и установить свойства как getters, чтобы можно было следить за тем, когда Angular проверяет значения.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
class Skill { constructor( private _id, private _name ) {} get id() { console.log('Checking id'); return this._id; } get name() { console.log('Checking name'); return this._name; } } @Component({ template: ` <my-select [options]="skills"></my-select> <button (click)="trigger()">Trigger change detection</button> ` }) export class AppComponent { skills = [ new Skill(1, 'JS'), new Skill(2, 'CSS'), new Skill(3, 'Angular') ] trigger() {} } |
При нажатии на кнопку Angular запустит цикл обнаружения изменений. Так как мы в режиме разработки, это будет происходить дважды за одно назначение. В нашем случае вычисления следующие:
1 2 3 4 5 6 7 |
option.id * 2 + option.name * 2 * 3 options = 12 |
Angular очень быстрый, однако с ростом вашего приложения Angular все сложнее отслеживать все изменения.
Что если бы мы могли помочь Angular и показать, когда нужно проверять компонент?
Мы можем задать ChangeDetectionStrategy нашего компонента в ChangeDetectionStrategy.OnPush. Это говорит Angular, что компонент зависит лишь от его Inputs и должен проверяться только в следующих случаях:
Меняется ссылка Input
В компоненте или его дочке произошло событие
Вы явно запускаете обнаружение изменений вызовом detectChanges()/tick()/markForCheck()
1 2 3 4 5 6 7 |
@Component({ selector: 'my-select', template: ` ... `, changeDetection: ChangeDetectionStrategy.OnPush }) |
Так как ни одно из описанных выше условий не выполняется, Angular не будет проверять компонент в текущем цикле определения изменений.
Использование TrackBy
Мы еще не закончили. Если в какой-то момент нам нужно будет изменить данные в коллекции (this.skills), возможно, в результате API-запроса, мы столкнемся с проблемой, так как Angular не может отслеживать элементы в коллекции и не знает о том, какой из них удален или добавлен.
Angular нужно удалить все элементы DOM, связанные с этими данными, и создать их заново. А это означает большое количество манипуляций с DOM, особенно если коллекция большая. А как мы знаем, манипуляции с DOM дорого обходятся.
Давайте посмотрим все в действии.
1 2 3 4 5 6 7 8 9 |
export class AppComponent { skills = [ ... ] ngOnInit() { setTimeout(() => { this.skills = [ ...same skills as before, new Skill(4, 'Typescript') ] }, 4000); } } |
Есть добавить функцию trackBy, Angular сможет отслеживать, какие элементы были добавлены или удалены по их уникальному идентификатору, а также сможет создавать и уничтожать то, что подверглось изменению.
Избегайте вычисляемых значений в шаблоне
Иногда необходимо трансформировать значение с сервера во что-либо, что можно отобразить в UI. Например:
1 2 3 4 5 6 7 8 9 10 11 |
@Component({ selector: 'skills', template: ` <table> <tr *ngFor="let skill of skills">{{skill.calcSomething(skill)}}</tr> </table> ` }) export class SkillsComponent { calcSomething(skill) { ... } } |
Проблема заключается в следующем: Angular нужно перезапускать вашу функцию в каждом цикле обнаружения изменений. Если функция выполняет что-то серьезное, она может сильно повлиять на производительность.
Если значение в процессе работы динамически не меняется, лучше всего будет:
Использовать чистые пайпы – Angular выполняет чистый пайп только, когда обнаруживает чистое изменение значения.
Генерировать новое свойство и задавать его значение один раз, например:
1 |
this.skills = this.skills.map(skill => ( { ...skill, percentage: calcSomething(skill) } ); |
Отключение обнаружения изменений
Представьте, что у вас есть компонент, который зависит от постоянно меняющихся данных, несколько раз в секунду.
Обновлять интерфейс при изменении данных будет довольно затратно. Лучше проверять и обновлять интерфейс каждые Х секунд.
Это можно сделать, отделив детектор изменений компонента, с помощью локальной проверки каждые х секунд.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@Component({ selector: 'giant-list', template: ` <li *ngFor="let d of dataProvider.data">Data {{d}}</lig> `, }) class GiantList { constructor(private ref: ChangeDetectorRef, private dataProvider: DataProvider) { ref.detach(); setInterval(() => { this.ref.detectChanges(); }, 5000); } } |
Использование ленивой загрузки
По моему мнению, ленивая загрузка – одна из мощнейших функций Angular и меньше всего используемых.
По умолчанию Webpack выводит весь код приложения в одно большую сборку. Ленивая загрузка дает возможность оптимизировать время загрузки приложения путем разбиения приложения на функциональные модули и загрузки по требованию.
Angular делает процесс почти прозрачным.
1 2 3 4 |
{ path: 'admin', loadChildren: 'app/admin/admin.module#AdminModule', } |
Мы даже можем отказаться от загрузки целых модулей при выполнении некоторых условий. Например, можно не загружать модуль admin, если пользователь не админ. (см. canLoad). Вот и все.
Автор: Netanel Basal
Источник: //netbasal.com/
Редакция: Команда webformyself.