От автора: помимо сроков также важна производительность разрабатываемого продукта. Ставки очень высоки, когда от вашей разработки зависит много пользователей. С большой силой и свободой приходит большая ответственность. JS предлагает нам множество библиотек и фреймворков, и обязанность разработчика заключается в том, чтобы максимально их использовать и сопоставлять с нуждами бизнеса, обеспечивая при этом пользователю доступ к интерфейсу.
Так учился и я. Разработка колоссального проекта с большим количеством пользователей никогда не было легкой прогулкой. Однако со временем мы смогли удовлетворить потребности наших пользователей наравне с другими приложениями на рынке, используя последние front-end технологии (Angular 2, Redux, ImmutableJS, webpack и т.д.). Ниже я расскажу про подводные камни, с которыми я столкнулся, чтобы вы могли обновить свое Angular приложение.
Следите за циклами
В любом приложении, которое вы откроете, будет много *ngFor циклов. Отследите эти циклы в месте их объявления, потому что любая глупость, которую вы собираетесь делать в повторяющемся компоненте, будет стоить вам больших потерь производительности.
Лучший способ оптимизировать циклы – отследить *ngFor с помощью свойства trackBy.
1 2 3 |
<ul> <li *ngFor="let song of songs; trackBy: trackSongByFn">{{song.name}}</li> </ul> |
Теперь можно создать функцию trackBy в ts файле:
1 2 3 |
trackByFn(index, song) { return index; // or song.id } |
Вы можете отслеживать список песен через индексы или через первичный ключ в модели данных, как обычный id. Определение trackBy в циклах Angular помогает идентифицировать строки, добавляемые или удаляемые при изменении модели/данных. Операции с DOM дорого обходятся для производительности, поэтому умным подходом будет не создавать в Angular все одноуровневые узлы в коллекции, если один элемент добавился, был удален или изменен. Механизм отслеживания позволяет Angular избегать таких проблем. Если вы до этого изучали ReactJS, то это то же, что и keys.
Полный переход на неизменяемость
Чтобы понять, что это значит, нужно понять, что так замедляет фреймворки. Догадываетесь?? Да, это постоянный ререндеринг. В декларативных фреймворках типа Angular, в которых вы привязываете значения динамической модели в своем шаблоне типа {{valueName}}, фреймворки каждый раз проделывает огромную работу, когда значение связанной переменной изменяется из любого места в коде. По мере роста приложения можете представить количество перерисовок на количество назначений! Т.е. нам не нужно использовать назначения? Нет, это абсурд.
Вместо этого давайте разберем механизм определения изменений в Angular. Подробное обсуждение уводит нас от темы. Поэтому я оставлю вам ссылку, чтобы вы сами все изучили.
Суть приведенной ссылки выше заключается в том, что мы должны передавать неизменяемые инпуты в компоненты и использовать ChangeDetectionStrategy.OnPush.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import { Component, Input } from '@angular/core'; import { ChangeDetectionStrategy } from '@angular/core'; import * as Immutable from 'immutable'; @Component({ selector: 'app-movie', template: ` <div> <h3>{{ title }}</h3> <p> <label>Actor:</label> <span>{{ actor.get('firstName') }} {{ actor.get('lastName') }}</span> </p> </div>`, changeDetection: ChangeDetectionStrategy.OnPush }) export class MovieComponent { @Input() title: string; @Input() actor: Immutable.Map<string, string>; } |
Следовательно, за исключением свойств инпутов примитивных типов, всегда необходимо использовать неизменяемые инпуты. На деле, все коммуникации с данными в вашем приложении должны проходить с использованием неизменных структур данных. Один из способов перейти на неизменность – использовать библиотеку Facebook* ImmutableJS. Она снизит количество перерисовок, а вы значительно улучшите производительность приложения.
Следите за обработчиками скролов
Когда речь заходит о производительности приложения или о гладком интерфейсе, один из ключевых факторов, определяющих UX – это производительность скролов. Нет ничего хуже, чем дергающийся экран. А вы когда-нибудь задумывались, почему экран дергается? Для решения проблемы необходимо определить ее источник. Можете почитать хорошую статью Paul Lewis.
В большинстве случаев производительность скролов страдает из-за тяжелых обработчиков события scroll. Событие scroll срабатывает крайне часто, и если заложить в него какие-то тяжелые операции с DOM, вы сразу получите трясущийся экран. Можно попробовать поместить часть вычислений с DOM над тяжелыми или динамическими элементами в обработчики скрола следующим образом.
1 2 3 4 5 6 7 |
let itemHolderEl = document.getElementById('itemHolder'); document.addEventListener('scroll', (e) => { console.log('e', e); window.getComputedStyle(itemHolderEl); itemHolderEl.getBoundingClientRect(); }); |
В моем случае я столкнулся с такими проблемами там, где должен был внедрить бесконечный скролл, т.е. когда вызов api должен быть автоматическим для загрузки следующего набора данных в список, когда пользователь достигает нижней границы страницы.
У меня есть класс infinite Scroll Directive в приложении, который применяет обработчик события скрола к передаваемому элементу.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import { Directive, Output, EventEmitter, HostListener } from '@angular/core'; @Directive({ selector: '[appInfiniteScroll]' }) export class InfiniteScrollDirective { @Output() reachedBottom: EventEmitter<any> = new EventEmitter(); @HostListener('scroll', ['$event']) onScroll(e) { let el = e.target; if (el.scrollHeight === (el.scrollTop + el.offsetHeight)) { this.reachedBottom.emit(); } } constructor() { } } |
Который я использую в прокручиваемом контейнере div вот так.
1 |
<div class="feed-holder" appInfiniteScroll (reachedBottom)="fetchNextData()"> |
Проблема в том, что проверка достижения нижней границы срабатывает каждый раз при малейшем скроле. В этом примере это не проблема, но представьте, что вам нужно использовать функцию getBoundingClientRect() или getComputedStyle. Это уже окажет серьезный эффект.
Значит ли это, что нам нельзя использовать эту логику внутри обработчиков? Можно, если делать это по-умному с добавлением щепотки RxJS.
Если есть способ запускать обработчик скрола только, когда пользователь остановился на несколько миллисекунд, то мы готовы. И да, RxJS – панацея с оператором debounceTime.
Что он делает – он не запускает событие в момент его появления, он запускает событие, когда есть пауза, превышающая заданное время debounceTime. Например, 20ms, как в примере выше. Т.е. вы видите, что пользователь остановился и ждет, что что-то произойдет. 20ms – достаточно быстро, поэтому негативного эффекта не будет. Вместо этого опыт улучшится, так как раньше событие срабатывало при любом скроле, а теперь только после определенной паузы.
1 2 3 4 5 6 7 8 |
ngAfterViewInit() { Observable.fromEvent(this.el.nativeElement, 'scroll') .debounceTime(20) .subscribe(res => { console.log('scroll', res); this.onScroll(res); }); } |
Проблема решена! Давайте посмотрим на график производительности.
Что?? Даже после добавления паузы у нас остались эти красные метки, вызывающие дергание экрана! Очевидно, что причина кроется в большом количестве скриптов, о чем свидетельствуют эти желтые песчаные дюны. Что же вызывает такое большое количество скриптов? Давайте копнем в эти дюны, чтобы понять это.
Эврика! Это механизм определения изменений Angular 2. Но почему он срабатывает на событие скрола. Мы не изменяем локальные переменные, пока не дойдем до паузы. Или изменяем?
Чтобы рассуждать на эту тему, нам нужно понять, как и когда обнаружение изменений запускает и обтекает процесс обнаружения изменений во всем дереве компонентов Angular. Вот красивое объяснение.
Из статьи: «Зоны подменяют глобальные асинхронные операции типа setTimeout() and addEventListener(),вот почему Angular узнает, когда нужно обновлять DOM»
Теперь поняли? Когда мы делаем это:
1 |
Observable.fromEvent(this.el.nativeElement, 'scroll') |
Метод внутренне добавляет обработчик события скрола и как-то zone js запускал цикл обнаружения изменений при любом скроле.
Так, преступника мы нашли. Какое же решение? Потерпите немного.
Сервис NgZone Angular, который является оберткой ZoneJS, предоставляет нам метод runOutsideAngular.
«Это позволяет избежать Angular zone и выполнять код так, чтобы он не вызывал обнаружение изменений, и обрабатывал ошибки Angular.»
1 2 3 4 5 6 7 8 9 10 11 12 |
constructor(private el: ElementRef, private zone: NgZone) { } ngAfterViewInit() { this.zone.runOutsideAngular(() => { Observable.fromEvent(this.el.nativeElement, 'scroll') .debounceTime(20) .subscribe(res => { console.log('scroll', res); this.onScroll(res); }); }); } |
После обертывания обработчика в NgZone runOutsideAngular мой таймлайн выглядит следующим образом.
Ура, зелень! Наконец мы можем попрощаться с этими красными отметками. А это означает быстрый и гладкий скрол.
Убирайте «грязь»
В наших ts или js файлах мы подписались или наблюдаем за Observables или Event Listener, но часто забываем отменить на них подписку, что дает нам не только неожиданные побочные эффекты, называемые ошибками, но также поглощает значительную долю производительности, особенно если компонент повторяется в цикле.
Как я и сказал, с большой свободой приходит большая ответственность. Всегда нужно чистить подписки, таймеры, обработчики событий в деструкторе, который является хуком жизненного цикла компонента в Angular 2 (ngDestroy).
1 2 3 4 5 6 7 8 9 10 11 12 |
ngOnDestroy() { this.dataSubscription.unsubscribe(); if (this.routeFragmentSubscription) { this.routeFragmentSubscription.unsubscribe(); } document.removeEventListener('click', <any>this.docHandler, true); clearInterval(this.pollingInterval); } |
Не используйте слишком много пайпов
Избегайте повторного использования async-пайпа в компоненте. Т.е. не делайте так:
1 2 3 4 5 6 |
<section *ngIf="!(data | async).get('isLoading')"> <h2>{{(data | async).get('heading')}}</h2> <ul> <li *ngFor="let item of (data | async).get('items')">{{item.get('name')}}</li> </ul> </section> |
Как видите, в примере выше (data | async) используется практически везде. Помните об этом, как и о множестве async-пайпов, которые вы напишите. На все это необходимы подписки, что означает еще большее количество обработчиков на запуск в любое время, когда происходит малейшее изменение. Поэтому не делайте так. Если попали в такую ситуацию, создайте дочерний компонент, тупой или компонент без состояния (на жаргоне Redux) и передайте в него данные в виде неизменяемого входного свойства после async-пайпа.
Заключение
Есть множество других причин, почему приложение может работать медленно. Эта статья лишь перечисляет причины и их решения для приложений, созданных на Angular 2 и выше. В некоторых случаях время загрузки приложения может быть большим из-за критического пути рендеринга или плохих практик программирования (например, при невыполнении советов выше). Теперь вы, по крайней мере, можете оптимизировать последнюю часть, которая, ухудшала UX или привела к потере пользователей. В маленьких приложениях эти советы можно игнорировать, однако в больших проектах я настоятельно рекомендую внедрять эти практики, чтобы улучшить UX. Благодарю! Хорошо покодить!
Автор: Param Singh
Источник: //medium.com/
Редакция: Команда webformyself.
* Признана экстремистской организацией и запрещена в Российской Федерации.