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

Это было вдохновлено Контрольным списком производительности Angular Минко Гечева:



mgechev / angular-performance-checklist
Шпаргалка по разработке« молниеносно
прогрессивных приложений Angular. - mgechev / angular-performance-checklist github.com »



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

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



1. ChangeDetectionStrategy.OnPush

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

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

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

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

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

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

Компакт-диск будет запущен в компоненте OnPush только в том случае, если входные данные изменились по ссылке.

2. Отсоединение детектора изменений

Каждый компонент в дереве проекта Angular имеет детектор изменений. Мы можем внедрить этот детектор изменений (ChangeDetectorRef), чтобы либо отсоединить компонент от дерева компакт-дисков, либо присоединить его к дереву компакт-дисков. Таким образом, когда 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()
    }
}

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

Это делает то:

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

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

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

С помощью TestComponent, как показано ниже:

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

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

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

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

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

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

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

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

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

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

@Component({
    ...
    template: `
        <div>
            {{data}}
            {{done}}
        </div>
    `
})
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, поэтому пользовательский интерфейс не обновляется. Мы хотим обновить пользовательский интерфейс, чтобы отображалось «Готово», когда данные равны или превышают 100, мы повторно вводим ngZone и устанавливаем для данных значение «Готово».

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

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

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

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

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

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

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

Чтобы добавить это поведение, нам нужно установить чистый флаг в аргументе литерала объекта декоратора @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 использует разницу, чтобы знать, когда есть изменение в итерируемом объекте, чтобы его можно было повторно отрисовать. В Differs для этого используется оператор строгой ссылки ===, который просматривает ссылки на объекты (т. е. адрес памяти).

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

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

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

Это заставит разработчика указать свою личность в итерации, которую будет отслеживать Differ. Это предотвратит постоянное уничтожение и воссоздание всей DOM.

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

Шаблонные выражения - это наиболее распространенная вещь, которую мы делаем в Angular.

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

@Component({
    template: `
        <div>
            {{func()}}
        </div>
    `
})
class TestComponent {
    func() {
        ...
    }
}

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

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

8. Веб-воркеры

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

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

Используя самоплагиат из моей предыдущей статьи Angular Performance: Web Worker:



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

Чтобы сгенерировать Web Worker, мы запускаем команду ng g web-worker:

ng g web-worker webworker

Это сгенерирует webworker.ts файл в src/app приложения Angular. web-worker сообщает инструментам CLI, что файл будет использоваться рабочим.

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

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

// webWorker-demo/src/app/app.component.ts
@Component({
    selector: 'app',
    template: `
        <div>
            <input type="number" [(ngModel)]="number" placeholder="Enter any number" />
            <button (click)="calcFib">Calc. Fib</button>
        </div>
        <div>{{output}}</div>
    `
})
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, не повлияет на производительность. Представьте прохождение ~ 10,000. Вот тогда мы начнем замечать снижение производительности. Как мы уже сказали, лучше всего переместить функцию или алгоритм Фибоначчи для выполнения в другом потоке. Таким образом, независимо от того, насколько велико число, оно не будет ощущаться в потоке 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: `
        <div>
            <input type="number" [(ngModel)]="number" placeholder="Enter any number" />
            <button (click)="calcFib">Calc. Fib</button>
        </div>
        <div>{{output}}</div>
    `
})
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, который мы создали ранее. Мы зарегистрировались для прослушивания сообщений, отправленных веб-воркером в onmessagehandler, любые полученные данные мы отобразим в DOM.

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

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

и обрабатывает число Фибоначчи, а затем отправляет результат обратно в поток DOM. Сообщение onmessage, которое мы создали в 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, затем загружается указанный блок.

Если размер всего пакета без отложенной загрузки составляет 1 МБ. При отложенной загрузке из 1 МБ будут вырезаны сведения о файле about и viewdetails, скажем, 300 и 500 КБ соответственно, мы увидим, что пакет будет сокращен до 200 КБ больше, чем наполовину от исходного размера !!!

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

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

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

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

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

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

Вот и все.

Заключение

Вот 10 лучших практик по оптимизации вашего приложения Angular.

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

Также помните, не оптимизируйте раньше. Создайте продукт, а затем найдите места для оптимизации.

Если у вас есть какие-либо вопросы относительно этого или чего-либо, что я должен добавить, исправить или удалить, не стесняйтесь комментировать, писать по электронной почте или писать мне в прямом сообщении.

Спасибо !!!

Получить мою электронную книгу

Я написал электронную книгу, которая объясняет многие концепции JavaScript в более простых терминах со ссылкой на EcmaSpec в качестве руководства:

Учить больше