Стимуляція продуктивності вашого застосування на фреймворку Angular 4

351

Від автора: крім термінів також важлива продуктивність розроблюваного продукту. Ставки дуже високі, коли від вашої розробки залежить багато користувачів. З великою силою і свободою приходить велика відповідальність. JS пропонує нам безліч бібліотек та фреймворків, і обов’язок розробника полягає в тому, щоб максимально їх використовувати і зіставляти з потребами бізнесу, забезпечуючи при цьому користувачеві доступ до інтерфейсу.

Так навчався і я. Розробка колосального проекту з великою кількістю користувачів ніколи не було легкою прогулянкою. Однак з часом ми змогли задовольнити потреби наших користувачів нарівні з іншими додатками на ринку, використовуючи останні front-end технології (Angular 2, Redux, ImmutableJS, webpack тощо). Нижче я розповім про підводні камені, з якими я зіткнувся, щоб ви могли відновити своє Angular додаток.

Слідкуйте за циклами

В будь-якому додатку, який ви відкриєте, буде багато *ngFor циклів. Відстежте ці цикли в місці їх оголошення, тому що будь-яка дурість, яку ви збираєтеся робити в повторюваному компоненті, буде коштувати вам великих втрат продуктивності.

Кращий спосіб оптимізувати цикли – відстежити *ngFor з допомогою властивості trackBy.

  • {{song.name}}

Тепер можна створити функцію trackBy в ts файлі:

trackByFn(index, song) {
return index; // or song.id
}

Ви можете відстежувати список пісень через індекси або через первинний ключ у моделі даних, як звичайний id. Визначення trackBy в циклах Angular допомагає ідентифікувати рядки, які додаються або видаляються при зміні моделі даних. Операції з DOM дорого обходяться для продуктивності, тому розумним підходом буде не створювати в Angular всі однорівневі вузли в колекції, якщо один елемент додався, був вилучений або змінений. Механізм відстеження дозволяє Angular уникати таких проблем. Якщо ви до цього вивчали ReactJS, то це те ж, що і keys.

Повний перехід на незмінюваність

Щоб зрозуміти, що це значить, потрібно зрозуміти, що так уповільнює фреймворки. Здогадуєтеся?? Так, це постійний ререндеринг. В декларативних фреймворках типу Angular, в яких ви прив’язуєте значення динамічної моделі у своєму шаблоні типу {{valueName}}, фреймворки кожен раз проробляє величезну роботу, коли значення зв’язаної змінної змінюється з будь-якого місця в коді. По мірі зростання додатки можете уявити кількість перерисовок на кількість призначень! Тобто нам не потрібно використовувати призначення? Ні, це абсурд.

Замість цього, давайте розберемо механізм визначення змін в Angular. Докладне обговорення відводить нас від теми. Тому я залишу вам посилання, щоб ви самі все вивчили.

Суть наведеної посилання вище полягає в тому, що ми повинні передавати незмінні инпуты в компоненти і використовувати ChangeDetectionStrategy.OnPush.

import { Component, Input } from ‘@angular/core’;
import { ChangeDetectionStrategy } from ‘@angular/core’;
import * as Immutable from ‘immutable”;
@Component({
selector: ‘app-movie’,
template: `

{{ title }}

Actor:
{{ actor.get(‘firstName’) }} {{ actor.get(‘lastName’) }}

`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MovieComponent {
@Input() title: string;
@Input() actor: Immutable.Map;
}

Отже, за винятком властивостей инпутов примітивних типів, завжди необхідно використовувати незмінні инпуты. Насправді, всі комунікації з даними у вашому додатку повинні проходити з використанням незмінних структур даних. Один із способів перейти на незмінність – використовувати бібліотеку Facebook ImmutableJS. Вона знизить кількість перерисовок, а ви значно поліпшите продуктивність програми.

Слідкуйте за обробниками скролов

Коли мова заходить про продуктивності додатка або про гладкому інтерфейсі, один з ключових чинників, що визначають UX – це продуктивність скролов. Немає нічого гірше, ніж смикається екран. А ви коли-небудь замислювалися, чому екран смикається? Для вирішення проблеми необхідно визначити її джерело. Можете почитати хорошу статтю Paul Lewis.

У більшості випадків продуктивність скролов страждає з-за важких обробників події scroll. Подія scroll спрацьовує дуже часто, і якщо закласти в нього якісь важкі операції з DOM, ви відразу отримаєте тремтячий екран. Можна спробувати помістити частину обчислень з DOM над важкими або динамічними елементами в обробники скрола наступним чином.

let itemHolderEl = document.getElementById(‘itemHolder’);
document.addEventListener. (‘scroll’, (e) => {
console.log(‘e’, e);
window.getComputedStyle(itemHolderEl);
itemHolderEl.getBoundingClientRect();
});

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

У мене є клас infinite Scroll Directive в додатку, який застосовує обробник події скрола до пакета елементу.

import { Directive, Output, EventEmitter, HostListener } from ‘@angular/core’;
@Directive({
selector: ‘[appInfiniteScroll]’
})
export class InfiniteScrollDirective {
@Output() reachedBottom: EventEmitter = new EventEmitter();
@HostListener(‘scroll’, [‘$event’])
onScroll(e) {
let el = e.target;
if (el.scrollHeight === (el.scrollTop + el.offsetHeight)) {
this.reachedBottom.emit();
}
}
constructor() { }
}

Який я використовую в прокручуваному контейнері div ось так.

Проблема в тому, що перевірка досягнення нижньої межі спрацьовує кожного разу при найменшому скроле. У цьому прикладі це не проблема, але уявіть, що вам потрібно використовувати функцію getBoundingClientRect() або getComputedStyle. Це вже зробить серйозний ефект.

Стимуляція продуктивності вашого застосування на фреймворку Angular 4

Чи означає це, що нам не можна використовувати цю логіку всередині обробників? Можна, якщо робити це по-розумному з додаванням щіпки RxJS.

Якщо є спосіб запускати обробник скрола тільки, коли користувач зупинився на кілька мілісекунд, то ми готові. І так, RxJS – панацея з оператором debounceTime.

Стимуляція продуктивності вашого застосування на фреймворку Angular 4

Що він робить – він не запускає подія в момент його появи, він запускає подія, коли є пауза, що перевищує заданий час debounceTime. Наприклад, 20ms, як у прикладі вище. Тобто ви бачите, що користувач зупинився й чекає, що щось станеться. 20ms – досить швидко, тому негативного ефекту не буде. Натомість досвід покращиться, так як раніше подія спрацьовувало при будь-якому скроле, а тепер тільки після певної паузи.

ngAfterViewInit() {
Вами.fromEvent(this.el.nativeElement, ‘scroll’)
.debounceTime(20)
.subscribe(res => {
console.log(‘scroll’, res);
this.onScroll(res);
});
}

Проблема вирішена! Давайте подивимося на графік продуктивності.

Стимуляція продуктивності вашого застосування на фреймворку Angular 4

Що?? Навіть після додавання паузи у нас залишилися ці червоні мітки, що викликають смикання екрану! Очевидно, що причина криється у великій кількості скриптів, про що свідчать ці жовті піщані дюни. Що ж викликає таку велику кількість скриптів? Давайте копнемо ці дюни, щоб зрозуміти це.

Стимуляція продуктивності вашого застосування на фреймворку Angular 4

Еврика! Це механізм визначення змін Angular 2. Але чому він спрацьовує на подію скрола. Ми не змінюємо локальні змінні, поки не дійдемо до паузи. Або змінюємо?

Щоб розмірковувати на цю тему, нам потрібно зрозуміти, як і коли виявлення змін запускає і обтікає процес виявлення змін у всьому дереві компонентів Angular. Ось гарне пояснення.

Зі статті: «Зони підміняють глобальні асинхронні операції типу setTimeout() and addEventListener. (), ось чому Angular дізнається, коли потрібно оновлювати DOM»

Тепер зрозуміли? Коли ми це робимо:

Вами.fromEvent(this.el.nativeElement, ‘scroll’)

Метод внутрішньо додає обробник події скрола і як-то zone js запускав цикл виявлення змін при будь-якому скроле.

Так, злочинця ми знайшли. Яке ж рішення? Потерпіть трохи.

Сервіс NgZone Angular, який є обгорткою ZoneJS, надає нам метод runOutsideAngular.

«Це дозволяє уникнути Angular zone і виконувати код так, щоб він не викликав виявлення змін, і обробляв помилки Angular.»

constructor(private el: ElementRef, private zone: NgZone) { }
ngAfterViewInit() {
this.zone.runOutsideAngular(() => {
Вами.fromEvent(this.el.nativeElement, ‘scroll’)
.debounceTime(20)
.subscribe(res => {
console.log(‘scroll’, res);
this.onScroll(res);
});
});
}

Після обгортання обробника в NgZone runOutsideAngular мій таймлайн виглядає наступним чином.

Стимуляція продуктивності вашого застосування на фреймворку Angular 4

Ура, зелень! Нарешті ми можемо попрощатися з цими червоними позначками. А це означає швидкий і гладкий скрол.

Прибирайте «бруд»

У наших ts або js файлах ми підписалися або спостерігаємо за Observables або Event Listener, але часто забуваємо скасувати на них підписку, що дає нам не тільки несподівані побічні ефекти, звані помилками, але також поглинає значну частку продуктивності, особливо якщо компонент повторюється в циклі.

Як я і сказав, з великою свободою приходить велика відповідальність. Завжди потрібно чистити підписки, таймери, обробники подій в деструкторе, який є хуком життєвого циклу компонента в Angular 2 (ngDestroy).

ngOnDestroy() {
this.dataSubscription.unsubscribe();
if (this.routeFragmentSubscription) {
this.routeFragmentSubscription.unsubscribe();
}
document.removeEventListener(‘click’, this.docHandler, true);
clearInterval(this.pollingInterval);
}

Не використовуйте занадто багато пайпов

Уникайте повторного використання async-пайпа у компоненті. Тобто не робіть так:

{{data | async).get(‘heading’)}}

  • {{item.get(‘name’)}}

Як бачите, в наведеному вище прикладі (data | async) використовується практично скрізь. Пам’ятайте про це, як і про безліч async-пайпов, які ви напишіть. На все це необхідні підписки, що означає ще більшу кількість обробників на запуск в будь-який час, коли відбувається найменшу зміну. Тому не робіть так. Якщо потрапили в таку ситуацію, створити дочірній компонент, тупий або компонент без стану (на жаргоні Redux) і передайте в нього дані у вигляді незмінного вхідного властивості після async-пайпа.

Висновок

Є безліч інших причин, чому програма може працювати повільно. Ця стаття лише перераховує причини і їх рішення для додатків, створених на Angular 2 і вище. У деяких випадках час завантаження програми може бути більшим за критичного шляху візуалізації або поганих практик програмування (наприклад, при невиконанні рад вище). Тепер ви принаймні можете оптимізувати останню частину, яка, погіршувала UX або призвела до втрати користувачів. У маленьких додатках ці поради можна ігнорувати, проте у великих проектах я настійно рекомендую впроваджувати ці практики, щоб поліпшити UX. Дякую! Добре покодить!