Від автора: сьогодні ми поговоримо про те, як оптимізувати продуктивність програми на Angular з допомогою ледачою завантаження, функцій TrackBy, OnPush, відключення виявлення змін і інших способів.
Використання OnPush
За замовчуванням Angular запускає механізм визначення змін на всіх компонентах кожен раз при зміні чого-небудь у додатку – від події click до отримання даних з ajax-запиту. (користувальницькі події, таймери, xhr, promise’и і т. д.)
Уявіть, наприклад, що у нас є компонент select.
@Component({
selector: ‘my-select’,
template: `
{{option.name}}
`
})
export class MySelectComponent {
@Input() options = [];
change() {}
}
Ми збираємося передати масив навичок в компонент select і встановити властивості як getters, щоб можна було стежити за тим, коли Angular перевіряє значення.
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: `
Trigger change detection
`
})
export class AppComponent {
skills = [ new Skill(1, ‘JS’), new Skill(2, ‘CSS’), new Skill(3, ‘Angular’) ]
trigger() {}
}
При натисканні на кнопку Angular запустить цикл виявлення змін. Так як ми в режимі розробки, це буде відбуватися двічі за одне призначення. У нашому випадку обчислення наступні:
option.id * 2
+
option.name * 2
*
3 options
=
12
Angular дуже швидкий, однак з ростом вашого додатки Angular все складніше відслідковувати всі зміни.
Що якщо б ми могли допомогти Angular і показати, коли потрібно перевіряти компонент?
Ми можемо задати ChangeDetectionStrategy нашого компонента в ChangeDetectionStrategy.OnPush. Це говорить Angular, що компонент залежить лише від його Inputs і повинен перевірятися тільки в наступних випадках:
Змінюється посилання Input
У компоненті або його доньці відбулася подія
Ви явно запускаєте виявлення змін викликом detectChanges()/tick()/markForCheck()
@Component({
selector: ‘my-select’,
template: `
…
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
Так як ні один з описаних вище умов не виконується, Angular не буде перевіряти компонент в поточному циклі визначення змін.
Використання TrackBy
Ми ще не закінчили. Якщо в якийсь момент нам потрібно буде змінити дані в колекції (this.skills), можливо, в результаті API-запиту, ми зіткнемося з проблемою, так як Angular не може відстежувати елементи в колекції, не знає про те, який з них вилучений або доданий.
Angular потрібно видалити всі елементи DOM, пов’язані з цими даними, і створити його заново. А це означає велику кількість маніпуляцій з DOM, особливо якщо велика колекція. А як ми знаємо, маніпуляції з DOM дорого обходяться.
Давайте подивимося все в дії.
export class AppComponent {
skills = [ … ]
ngOnInit() {
setTimeout(() => {
this.skills = [ …same skills as before, new Skill(4, ‘Typescript’) ]
}, 4000);
}
}
Є додати функцію trackBy, Angular зможе відстежувати елементи, які були додані або видалені з їх унікальним ідентифікатором, а також зможе створювати і знищувати те, що піддалося зміні.
Уникайте обчислюваних значень в шаблоні
Іноді необхідно трансформувати значення з сервера у що-небудь, що можна відобразити в UI. Наприклад:
@Component({
selector: ‘skills’,
template: `
`
})
export class SkillsComponent {
calcSomething(skill) { … }
}
Проблема полягає в наступному: Angular потрібно перезапускати вашу функцію в кожному циклі виявлення змін. Якщо функція виконує щось серйозне, вона може сильно вплинути на продуктивність.
Якщо значення в процесі роботи динамічно не міняється, краще всього буде:
Використовувати чисті пайпи – Angular виконує чистий пайп тільки, коли виявляє чиста зміна значення.
Генерувати нове властивість і ставити його значення один раз, наприклад:
this.skills = this.skills.map(skill => ( { …skill, percentage: calcSomething(skill) } );
Відключення виявлення змін
Уявіть, що у вас є компонент, який залежить від постійно мінливих даних, кілька разів в секунду.
Оновлювати інтерфейс при зміні даних буде досить затратно. Краще перевіряти та оновлювати інтерфейс кожні Х секунд.
Це можна зробити, відокремивши детектор змін компонента, з допомогою локальної перевірки кожні х секунд.
@Component({
selector: ‘giant-list’,
template: `
`,
})
class GiantList {
constructor(private ref: ChangeDetectorRef, private dataProvider: DataProvider) {
ref.detach();
setInterval(() => {
this.ref.detectChanges();
}, 5000);
}
}
Використання ледачою завантаження
На мою думку, лінива завантаження – одна з найпотужніших функцій Angular і найменше використовуються.
За замовчуванням Webpack виводить весь код програми в одну велику збірку. Лінива завантаження дає змогу оптимізувати час завантаження програми шляхом розбиття програми на функціональні модулі та завантаження на вимогу.
Angular робить процес майже прозорим.
{
path: ‘admin’,
loadChildren: ‘app/admin/admin.module#AdminModule’,
}
Ми навіть можемо відмовитися від завантаження цілих модулів при виконанні деяких умов. Наприклад, можна не завантажувати модуль admin, якщо користувач не адмін. (див. canLoad). От і все.