DevGang
Авторизоваться

10 хитростей для оптимизации вашего Angular приложения 

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

Прежде чем продолжить работу с нашим списком, советую оптимизировать подход к программированию (а не только код): попробуйте использовать Bit ( Github ) для совместного использования и повторного использования компонентов Angular, чтобы быстрее создавать приложения с единым пользовательским интерфейсом.

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

1. ChangeDetectionStrategy.OnPush

Обнаружение изменений является одной из наиболее распространенных функций, обнаруженных в JS и его инфраструктурах. Это возможность определить, когда данные пользователя изменились, обновить DOM, чтобы отразить изменения.

Angular использовал Zone.js для обезвреживания каждого асинхронного события, поэтому всякий раз, когда происходит какое-либо событие, Angular запускает обнаружение изменений в своем дереве компонентов.

У компонентов есть входные данные, которые они используют для получения данных от своих родительских компонентов. Когда происходит асинхронное событие, Angular анализирует дерево компонентов и проверяет входные данные на наличие любых отличий от его предыдущего значения. Эта проверка любых различий выполняется с помощью оператора строгого равенства. Этот оператор проверяет изменение ссылок на входах компонента, поэтому для текущих значений ввода было сделано новое выделение памяти.

С этим Angular принес стратегии обнаружения изменений: Default и OnPush.

Эта стратегия обнаружения изменений OnPush запрещает запуск CD на компоненте и его дочерних элементах. Когда приложение загружается, Angular запускает CD на компоненте OnPush и отключает его. При последующих запусках CD компонент OnPush пропускается вместе со своими дочерними компонентами в поддереве.

CD будет запущен на компоненте OnPush, только если входные данные были референциально изменены.

2. Отключение детектора изменений

Каждый компонент в дереве проекта Angular имеет детектор изменений. Мы можем внедрить этот детектор изменений (ChangeDetectorRef), чтобы либо отсоединить компонент от дерева CD, либо прикрепить его к дереву CD. Таким образом, когда Angular запускает CD на дереве компонентов, компонент с его поддеревом будет пропущен.

Это делается с помощью класса ChangeDetectorRef.

export abstract class ChangeDetectorRef {
  abstract markForCheck(): void;
  abstract detach(): void;
  abstract detectChanges(): void;
  abstract checkNoChanges(): void;
  abstract reattach(): void;
}

Смотрите методы:

markForCheck: когда представление использует стратегию обнаружения изменений OnPush (checkOnce), явно помечает представление как измененное, чтобы его можно было снова проверить. Компоненты обычно помечаются как загрязненные (нуждающиеся в повторном рендеринге), когда входные данные изменились или в представлении возникли события. Вызовите этот метод, чтобы убедиться, что компонент проверен, даже если эти триггеры не произошли.

detach: отсоединяет это представление от дерева обнаружения изменений. Отдельный компонент не проверяется, пока он не будет присоединен. Используйте в сочетании с detectChanges() для реализации локальных проверок обнаружения изменений. Отдельные компонент не проверяются во время прогонов обнаружения изменений, пока они не будут повторно присоединены, даже если они помечены как грязные.

detectChanges: проверяет это представление и его дочерние элементы. Используйте в сочетании с detach для реализации локальных проверок обнаружения изменений.

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

reattach: повторно присоединяет ранее отключенное представление к дереву обнаружения изменений. Представления привязаны к дереву по умолчанию.

Пример, у нас есть этот компонент:

@Compoennt({
    ...
})
class TestComponent {
    constructor(private changeDetectorRef: ChangeDetectorRef) {
        chnageDetectorRef.detach()
    }
}

Мы вызвали отсоединение от конструктора, потому что это точка инициализации, поэтому компонент при запуске отсоединяется от дерева компонентов. CD запускается по всему дереву компонентов, не влияет на TestComponent. Если мы изменим привязанные к шаблону данные в компоненте, нам нужно повторно подключить компонент, чтобы DOM обновлялся при следующем запуске CD.

@Component({
    ...
    template: `
{{data}}
` }) class TestComponent { data = 0 constructor(private changeDetectorRef: ChangeDetectorRef) { changeDetectorRef.detach() } clickHandler() { changeDetectorRef.reattach() data ++ } }

3. Обнаружение локальных изменений

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

С TestComponent, как показано ниже:

@Component({
    ...
    template: `
{{data}}
` }) class TestComponent { data = 0 constructor(private changeDetectorRef: ChangeDetectorRef) { changeDetectorRef.detach() } }

Мы можем обновить свойство данных с привязкой к данным и использовать метод detectChanges для запуска CD только для TestComponent и его дочерних элементов.

@Component({
    ...
    template: `
{{data}}
` }) class TestComponent { data = 0 constructor(private changeDetectorRef: ChangeDetectorRef) { changeDetectorRef.detach() } clickHandler() { data ++ chnageDetectorRef.detectChnages() } }

Метод clickHandler увеличит значение данных на единицу и вызовет detectChanges для запуска CD на TestComponent и его дочерних элементах. Это приведет к тому, что данные будут обновлены в DOM, пока они будут отсоединены от дерева CD.

Локальный CD запускается от компонента до его дочерних элементов, в отличие от глобального CD, который запускается от корневого до дочерних компонентов.

Это даст огромную производительность, если переменная данных обновляется каждую секунду.

4. Запуск вне Angular

Мы знаем, что NgZone / Zone - это то, что Angular использует для подключения к асинхронным событиям, чтобы знать, когда запускать CD в дереве компонентов. Теперь, когда весь код, который мы пишем в Angular, выполняется в зоне Angular, эта зона создается Zone.js для прослушивания асинхронных событий и передачи их Angular.

Angular имеет эту функцию, которая позволяет нам запускать кодовые блоки вне этой Angular зоны. Теперь, в этой внешней Angular зоне, асинхронные события больше не воспринимаются NgZone / Zone, поэтому любое генерируемое асинхронное событие для него не запускается. Это означает, что пользовательский интерфейс не будет обновлен.

Это очень полезно, если мы запускаем код, который изменяет интерфейс каждую секунду. Вы увидите, что сначала лучше не обновлять пользовательский интерфейс, а подождать, когда вы захотите отобразить данные, которые вы повторно вводите в Zone.

@Component({
    ...
    template: `
        
{{data}} {{done}}
` }) class TestComponent { data = 0 done constructor(private ngZone: NgZone) {} processInsideZone() { if(data >= 100) done = "Done" else data += 1 } processOutsideZone() { this.ngZone.runOutsideAngular(()=> { if(data >= 100) this.ngZone.run(()=> {data = "Done"}) else data += 1 }) } }

processInsideZone запускает код внутри Angular, поэтому пользовательский интерфейс обновляется при запуске метода.

processOutsideZone запускает код вне зоны ng, поэтому пользовательский интерфейс не обновляется. Мы хотим обновить интерфейс для отображения «Done», когда данные равны или больше 100, мы повторно вводим ngZone и устанавливаем данные в «Done».

5. Используйте чистые Pipe

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

Просто представьте, что у нас есть функция @Pipe, которая занимает много времени, прежде чем вы получите результат.

function bigFunction(val) {
    ...
    return something
}
@Pipe({
    name: "util"
})
class UtilPipe implements PipeTransform {
    transform(value) {
        return bigFunction(value)
    }
}

Мы увидим, что эта функция повесит основной поток, который запускает пользовательский интерфейс, и сделает его медленным для пользователей. Что еще хуже, канал вызывается каждую секунду, что станет для пользователей адским опытом.

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

Поэтому независимо от того, сколько раз канал вызывается с помощью ввода, функция bigFunction вызывается один раз, а результаты из кэша просто возвращаются при последующих вызовах.

Чтобы добавить это поведение, нам нужно установить флаг pure в аргументе объекта декоратора @Pipe в значение true.

function bigFunction(val) {
    ...
    return something
}
@Pipe({
    name: "util",
    pure: true
})
class UtilPipe implements PipeTransform {
    transform(value) {
        return bigFunction(value)
    }
}

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

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

6. Используйте опцию trackBy для директивы *ngFor

*ngFor используется для повторения итераций и их рендеринга в DOM. Тем не менее, он очень полезен благодаря узким местам производительности.

Внутренне, ngFor использует отличия, чтобы знать, когда есть изменение в итерируемом объекте, так что он может повторно выполнить рендеринг. для этого используется оператор строгой ссылки ===, который просматривает ссылки на объекты (т. е. адрес памяти).

Сопоставив это с практикой неизменяемости, мы увидим, что мы будем ломать ссылки на объекты, что приведет к тому, что ngFor будет постоянно разрушать и заново создавать DOM на каждой итерации.

Это не будет проблемой для 10-100 элементов в цикле, но будет при 1000 и более, что серьезно повлияет на производительность пользовательского интерфейса.

У ngFor есть опция trackBy (или, я бы сказал, опция для Differs), которую он использует для отслеживания идентичности элементов в итерируемом объекте.

Это заставит разработчика указать свою идентичность в итерируемом объекте для отслеживания различия. Это предотвратит постоянное разрушение и воссоздание всего DOM.

7. Оптимизируйте выражения шаблона

Шаблонные выражения - самое распространенное, что мы делаем в Angular.

Мы часто запускаем функции в шаблонах:

@Component({
    template: `
        
{{func()}}
` }) class TestComponent { func() { ... } }

Теперь эта функция будет запускаться при запуске CD на TestComponent. Кроме того, эта функция должна быть завершена до того, как CD и другие коды будут двигаться дальше.

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

8. Web Worker

JS - это однопоточный язык, это означает, что код JS выполняется в основном потоке. Этот основной поток запускает алгоритм пользовательского интерфейса.

Теперь, если алгоритм не-UI станет тяжелым, мы увидим, что он повлияет на поток UI, замедляя его. Web Worker - это добавленная функция, которая позволяет нам создавать и запускать код в другом потоке.

С помощью Web Workers в Angular инструмент CLI упрощал его настройку, компиляцию, пакетирование и разбиение кода.

Чтобы создать Web Worker, мы запускаем команду:

ng g web-worker webworker

Это создаст файл webworker.ts в src/appweb-worker сообщает инструментам CLI, что файл будет использовать Worker.

Чтобы продемонстрировать, как использовать Web Worker в Angular для оптимизации его производительности. Допустим, у нас есть приложение, которое вычисляет числа Фибоначчи. Поиск чисел Фибоначчи в потоке DOM отчасти повлияет на работу пользовательского интерфейса, потому что взаимодействие DOM и пользователя будет зависать до тех пор, пока число не будет найдено.

Начиная наше приложение будет выглядеть так:

// webWorker-demo/src/app/app.component.ts
@Component({
    selector: 'app',
    template: `
        
{{output}}
` }) export class App { private number private output calcFib() { this.output =fibonacci(this.number) } } function fibonacci(num) { if (num == 1 || num == 2) { return 1 } return fibonacci(num - 1) + fibonacci(num - 2) }

Вычисление чисел Фибоначчи является рекурсивным, передача небольших чисел, таких как 0–900, не повлияет на производительность. Вообразите прохождение ~ 10000. Вот когда мы начнем замечать замедление производительности. Как мы уже говорили, лучше всего переместить функцию или алгоритм Фибоначчи в другой поток. Поэтому независимо от того, насколько велико это число, оно не будет ощущаться в потоке DOM.

Итак, мы создадим файл Web Worker:

ng g web-worker webWorker

и переместите функцию Фибоначчи в файл:

// webWorker-demo/src/app/webWorker.ts
function fibonacci(num) {
    if (num == 1 || num == 2) {
        return 1
    }
    return fibonacci(num - 1) + fibonacci(num - 2)
}
self.addEventListener('message', (evt) => {
    const num = evt.data
    postMessage(fibonacci(num))
})

Теперь мы отредактируем app.component.ts, чтобы добавить Web Worker.

// webWorker-demo/arc/app/app.component.ts
@Component({
    selector: 'app',
    template: `
        
{{output}}
` }) export class App implements OnInit{ private number private output private webworker: Worker ngOnInit() { if(typeof Worker !== 'undefined') { this.webWorker = new Worker('./webWorker') this.webWorker.onmessage = function(data) { this.output = data } } } calcFib() { this.webWorker.postMessage(this.number) } }

Мы добавили хук жизненного цикла ngOnInit в наш компонент, чтобы инициализировать Web Worker, который мы сгенерировали ранее. Мы зарегистрировались для прослушивания сообщений, отправленных Web Worker в обработчике onmessage, любые полученные данные будут отображаться в DOM.

Мы сделали функцию calcFib для отправки числа в Web Worker. Это ниже в WebWorker будет захватывать число

self.addEventListener('message', (evt) => {
    const num = evt.data
    postMessage(fibonacci(num))
})

и обрабатывает число Фибоначчи, а затем отправляет результат обратно в поток DOM. Событие, которое мы настроили в app.component

ngOnInit() {
    if(typeof Worker !== 'undefined') {
        this.webWorker = new Worker('./webWorker')
        this.webWorker.onmessage = function(data) {
            this.output = data
        }
    }
}

получит результат data, затем мы отобразим результат в DOM, используя {{output}}.

Во время обработки чисел Фибоначчи поток DOM будет сосредоточен на взаимодействиях с пользователем, в то время как webWorker будет выполнять тяжелую обработку.

9. Ленивая загрузка

Ленивая загрузка - один из самых популярных и эффективных приемов оптимизации в браузере. Он включает в себя откладывание загрузки ресурсов (изображений, аудио, видео, веб-страниц) во время загрузки до момента, когда это необходимо, затем загружается.

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

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

const routes: Routes = [
    {
        path: '',
        component: HomeComponent
    },
    {
        path: 'about',
        loadChildren: ()=> import("./about/about.module").then(m => m.AboutModule)
    },
    {
        path:'viewdetails',
        loadChildren: ()=> import("./viewdetails/viewdetails.module").then(m => m.ViewDetailsModule)
    }
]
@NgModule({
    exports: [RouterModule],
    imports: [RouterModule.forChild(routes)]
})
class AppRoutingModule {}

Мы используем динамический импорт, чтобы сообщить Angular маршрутам, что мы хотим ленивую загрузку. Angular сгенерирует отдельный фрагмент для about и viewdetails. При первоначальной загрузке приложения блок about и viewdetails не загружается, когда пользователь хочет перейти к маршруту about или viewdetails, затем загружается указанный блок.

Если размер всего загруженного без ленивых данных пакета составляет 1 МБ. Lazy-loading будет склеивать детали about и view из 1MB, скажем, они составляют 300kb и 500kb соответственно, мы увидим, что пакет будет сокращен до 200kb больше, чем половина исходного размера !!!

10. Предварительная загрузка

Это стратегия оптимизации, которая загружает ресурсы (веб-страницы, аудио, видео файлы) для более быстрой навигации или потребления в будущем. Это ускоряет как загрузку, так и рендеринг ресурса, поскольку ресурс уже будет присутствовать в кэше браузера.

В Angular реализована стратегия предварительной загрузки в модуле @angular/router. Это позволяет нам предварительно загружать ресурсы, маршруты / ссылки, модули и т.д. в приложениях Angular. Маршрутизатор Angular предоставляет абстрактный класс PreloadingStrategy:

class OurPreloadingStrategy implements PreloadingStrategy {
    preload(route: Route, fn: ()=> Observable ) {
        // ...
    }
}

Мы указываем его как значение для свойства preloadingStrategy в конфигурации маршрутизатора.

// ...
RouterModule.forRoot([
    ...
], {
    preloadingStrategy: OurPreloadingStrategy
})
// ...

Вот и все.

Вывод

Здесь представлены 10 лучших рекомендаций по оптимизации вашего приложения Angular.

Эти практики не должны быть все реализованы в вашем приложении Angular, просто стоит знать каждый из них и знать, когда их применять.

Кроме того, помните, не оптимизируйте рано. Создайте продукт, а затем найдите места для оптимизации.

Источник:

#Angular
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

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

Попробовать

Освой перспективную онлайн профессию!

Получить скидку