65.9K
CodeProject 正在变化。 阅读更多。
Home

加速您的 Angular 应用 - 14 个 Angular 优化技巧

2020年4月15日

CPOL
viewsIcon

5707

在本文中,我们将向您展示如何使您的 Angular 应用更小、更快、响应更灵敏。

将您的应用程序更新到 Angular 9 只是实现整体性能优化的第一步。作为一名勤奋的开发人员,您应该关注其他可以优化 Angular 应用性能的领域:构建和部署时间、代码优化以及在运行时监控应用的运维技巧。

于 2019 年最后一个季度发布的 Angular 9 引入了多项新功能,例如无选择器指令、依赖注入变更以及 TypeScript 诊断改进。其中最重要的是 Ivy 编译器,它显著减小了 Angular 应用的包体大小。它还减少了下载时间,解决了之前版本 Angular 中影响应用程序性能的一些问题。

在本文中,我们将向您展示如何使您的 Angular 应用更小、更快、响应更灵敏。

在这里,我们将讨论

  1. 启用生产模式
  2. AOT
  3. Minification
  4. 避免在视图中调用函数和 getter
  5. 纯管道 (Pure pipes)
  6. 懒加载模块
  7. 代码分割
  8. OnPush 变更检测
  9. 异步管道 (Async pipe)
  10. ngDoCheck
  11. Track by 函数
  12. Zone.js
  13. 取消订阅 observables
  14. Web workers

让我们深入探讨。

为您的 Angular 应用提速

正如 Angular 专家 Bonnie Brennan 所说,“要想从 Angular 中获得最佳性能,你需要把它看作一辆跑车,而不是一辆皮卡。” 这意味着 Angular 应该非常精简和快速,所以要让它自由运行。例如,它不必检测模型中每一项数据的变化。

构建和部署优化

生产构建

在部署您的应用程序之前,请确保创建了生产构建。此模式会执行许多在开发构建中不可用的重要优化,包括预编译 (ahead-of-time, AOT)、代码压缩 (minification) 和摇树优化 (tree-shaking)。

JIT vs. AOT

Angular 提供了两种编译模型:即时编译(JIT),它在运行时编译您的应用;以及预编译(AOT),它在构建时进行编译。默认情况下,开发编译使用 JIT 编译,这要求您包含 Angular 编译器。

AOT 在构建时进行编译,只生成已编译的模板,并从部署包中移除了 Angular 编译器,这会使您的应用负载减少大约 1MB(大致是 Angular 编译器的大小)。

您可以使用带有 --AOT 开关的 CLI 命令进行编译,以利用 AOT 的优化功能。

ng build --aot 
ng serve --aot

Minification

我们的 JavaScript 代码中的许多字符,包括空格、换行符、注释和块分隔符,仅用于可读性和视觉目的。它们对于代码的正确运行并非必需。代码压缩过程会移除这些字符,简化名称,并忽略不可达的代码。通过压缩代码,您可以加快页面的下载和执行时间。

现在,比较一下这段代码,在压缩前

var app = angular.module("myApp", []);
    app.controller("myCtrl", function($scope) {
    $scope.title = "Lorem";
    $scope.subtitle = "Ipsum";
    if (false) {
        console.log('Lorem Ipsum Dolor');
    }
});

和这段代码在压缩后

var app=angular.module("myApp",[]);app.controller("myCtrl",function(l){l.title="Lorem",l.subtitle="Ipsum"});

构建优化器

由 Angular 团队创建的构建优化器 (Build Optimizer) 是一个进一步优化 Angular Webpack 构建的工具。它能识别出可以在构建时移除而无副作用的代码。例如,构建优化器可以从 AOT 构建中移除像 @Component 这样的 Angular 装饰器。由于编译器从这些装饰器中提取了所有必要信息,所以它们仅在编译时需要。

您可以浏览该项目的 GitHub 仓库主页来了解 Angular 构建优化器是如何实现这些转换的。

使用 Angular 优化创建生产构建

如果您执行生产构建,上述优化将会被应用。以下 CLI 指令通过创建生产构建,实现简单的部署

ng build --prod

之后,生成的输出目录可以被复制到 Web 服务器上。如果您使用了 prod 标志,您可能会开始看到在没有它时不会出现的错误。但这是件好事:现在您有机会捕获并解决那些否则只会在运行时才出现的错误。

启用生产模式

默认情况下,Angular 在调试模式下运行,这会添加一些断言检查,但也会每次运行两次 `ChangeDetection` 以确保绑定值没有意外的更改。为了只调用一次 `ChangeDetection`,您需要启用生产模式,通过将以下代码添加到您的 Angular 应用中

import { enableProdMode } from '@angular/core';
import { environment } from './environments/environment';

if (environment.production) {
    enableProdMode();
}

有用的 Angular 演示

作家兼谷歌开发者专家 Alexey Zuev 构建了一个在线 IDE,它开箱即用地支持 Angular 开发项目。您可以访问该网站,用它在线测试 Angular AOT 和 JIT 编译。在那里,ng-run 会立即对示例应用程序执行预编译

AOT 编译后,应用被部署,但不包含 Angular 编译器

现在让我们通过关闭 Ivy AOT 开关来测试 JIT 编译

当我们将编译模式更改为 JIT 时,唯一的区别似乎是应用程序启动时没有了“*Ivy AOT compilation…*”的消息。然而,当我们打开 Chrome 工具来比较下载的文件时,我们看到 JIT 模式要求应用程序包含 Angular 编译器和相关包,而当应用使用 AOT 预编译时,则无需下载这些文件

Angular 代码优化

懒加载模块

Angular 提供了懒加载功能,这是一种按需加载页面的简单方法。通过懒加载,模块仅在用户导航到该特定模块的路由时才加载。Angular 团队将此功能内置到路由器中,所有工作都在后台为您完成。因此,懒加载使用起来非常直接。

然而,有一个被忽视但对于利用懒加载至关重要的事实是:它需要更多的模块。将应用程序划分为多个模块的一个好处是能够按需加载模块。如果您编写了 30,000 行代码并且它们都在一个模块中,您将无法利用懒加载功能,您的应用可能会变得越来越慢。

这是一个没有懒加载的路由配置示例

const routes: Routes = [
  {
    path: 'customers',
    loadChildren: CustomersComponent
  },
  {
    path: 'orders',
    loadChildren: OrdersComponent
  }
];

以及应用了懒加载后的相同路由配置

const routes: Routes = [
  {
    path: 'customers',
    loadChildren: () => import('./customers/customers.module').then(m => m.CustomersModule)
  },
  {
    path: 'orders',
    loadChildren: () => import('./orders/orders.module').then(m => m.OrdersModule)
  }
];

代码分割

减少初始加载时间和加快页面导航的一种方法是代码分割。随着 Web 应用程序变得越来越复杂,发送给用户的 JavaScript 文件也越来越大。大型 JavaScript 文件可能会延迟浏览器中的交互时间,特别是对于移动用户。

代码分割可以有效地减少您应用中的 JavaScript 包,而不会损失任何功能。这项技术让您可以将 JavaScript 代码分解成多个部分,这些部分可以在用户导航到不同路由时,或者当用户打开或展开组件时逐步加载。

主要有两种方法:组件级代码分割,即使没有路由导航,单个组件也可以被懒加载;以及路由级代码分割,其中单个路由被懒加载。

OnPush 变更检测

默认情况下,Angular 会检查每个组件以查看是否有更改,并相应地更新视图。虽然这是一个相对快速的过程,但随着您的应用程序的增长,这些频繁的更新检查会变得越来越慢。

与默认策略(只要应用中有变化就检查组件)不同,OnPush 变更检测只对 `@input` 参数的变化做出反应,或者当您手动触发检测时才会反应。

要启用 OnPush 变更检测,请在组件装饰器中定义此策略

import { ChangeDetectionStrategy, Component, Input } from '@angular/core';

@Component({
    selector: 'app-user-list',
    templateUrl: './user-list.component.html',
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserListComponent {
    @Input() users$: any[];
    trackByFn(index: number, item: any): any {
        return item.id;
    }
}

ngDoCheck 生命周期钩子

每次变更检测运行时,Angular 都会调用 `ngDoCheck`。因此,`ngDoCheck` 是在您的组件中添加计算密集型或缓慢的自定义逻辑,或检测 Angular 可能会忽略的变化的理想位置

    ngDoCheck() {
        const cartChanges = this.differ.diff(this.shoppingCart);

        if (cartChanges) {
            console.log(cartChanges);
            cartChanges.forEachChangedItem(r => ...);
            cartChanges.forEachAddedItem(r => ...);
            cartChanges.forEachRemovedItem(r => ...);
        }

    }

在这里,`ngDoCheck` 用于将 `differ.diff` 方法应用于集合的当前值。然后,differ 会将该集合与其先前的值进行比较,并返回更改列表。

您可以将复杂的计算移到 `ngDoCheck` 生命周期钩子中,并在视图中引用计算出的值。请记住,缓存复杂计算的结果会带来更好的性能。

异步管道 (Async Pipe)

当使用 observables 时,调用 subscribe 方法却忘记调用 unsubscribe,随后会导致内存泄漏。内存泄漏是为什么异步管道 (async pipe) 是您的好朋友:它为您处理所有的清理工作。它不仅为您订阅,还会在您关闭组件时负责取消订阅,并且每次更新时都会调用 markForCheck。因此,异步管道是 OnPush 变更检测策略的完美搭档。

异步管道使您能够直接在视图中使用 RxJS observables。正如您在下面的代码中看到的,每当异步管道更新一个值时,它都会自动为您调用 `markForCheck`。因此,您只需注入您的数据服务,而不需要将实际值放在组件的字段中。相反,您添加一个对 observable 的引用。现在,当有更新发送到这个 observable 时,您的组件也会检测到变化

<span>Wait for it... {{ greeting | async }}</span>

避免在视图中调用函数和 getter

当绑定到一个对象时,Angular 在模型属性中执行变更验证非常快,因为它不需要执行任何函数。然而,当您绑定到一个函数或 JavaScript getter 时,Angular 必须运行您的函数来检查值是否已更改。在某些情况下,Angular 会频繁地执行变更检测,导致您的应用出现严重的性能问题。

为避免此类问题,如果可以避免,切勿在 Angular 模板表达式中绑定函数或 getter。相反,使用纯管道 (pure pipes) 让 Angular 在值未改变时高效地跳过管道执行。您也可以在组件的控制器中手动计算值,并在需要时重新计算它们。

export class MovieComponent {
    header: string = 'Movie details';
    title: string;
    synopsis: string;
    duration: number;

    constructor(http: HttpClient) {
        http.get('https://my-movie-database.com/api/movie/'
        , (result) => {
            this.title = result.title;
            this.synopsis = result.synopsis;
            this.duration = result.duration;
        });
    }
}

纯管道 (Pure Pipes)

有时,您需要在视图中调用一个函数,但在许多情况下,您可以使用管道来代替。请注意,纯管道函数必须实现 `PipeTransform`

import {Pipe, PipeTransform} from '@angular/core';

@Pipe({name: 'repeat'})
export class RepeatPipe implements PipeTransform {
  transform(value: any, times: number) {
    return value.repeat(times);
  }
}

这可以在模板中这样实现

<span> {{ 'Bla' | repeat:5 }}</span>

该模板将呈现为

<span>BlaBlaBlaBlaBla</span>

Zone.js

有时,您的视图没有更新,后来您发现当模型被异步函数(如 setInterval、setTimeout、鼠标事件或 promise rejection)更新时,Angular 不会检测到变化。为了解决这个问题,人们求助于 AngularJS 中这些异步函数的替代方案,但他们仍然需要以编程方式更新视图。

多年前,Angular 2 引入了 Zone.js,作为一种通过自动变更检测来修补这些异步浏览器函数的方法。有了 Zone.js,您的异步函数能够自动更新视图。这是一个受欢迎的增强功能,但它也意味着每一次异步执行都会触发一次变更检测。

`ngZone` 就是在这里派上用场的。在您确定了哪些异步函数不影响模型且不需要更新视图后,您可以告诉 ngZone 在 Angular 上下文之外运行这些函数

export class AppComponent {
  constructor (ngZone: NgZone) {
     ngZone.runOutsideAngular(() => {
      // runs outside Angular zone, for performance-critical code

      ngZone.run(() => {
      // runs inside Angular zone, for updating view afterwards
     });
   });
  }
}

在这个 Stackblitz 演示中,您可以看到在 Angular zone 之外运行一个循环不会导致 UI 在每个 setTimeout 周期后刷新

取消订阅 Observables

取消订阅很简单:您存储订阅,然后使用 `ngOnDestroy` 生命周期钩子函数在订阅对象上调用 unsubscribe。

幸运的是,`ngOnDestroy` 生命周期钩子创建了一个很好的模式,因此您可以在订阅期间适当地处理内存。对于每个组件或指令,使用 `ngOnDestroy` 回调方法,在其中为您的订阅调用 unsubscribe

import { OnDestroy } from '@angular/core';

export class MyCleanupComponent implements OnDestroy {
    private _subs: Subscription;

    constructor(router: Router) {
        this._subs = router.events.subscribe(event => {
            //Event must be handled here...
        });
    }

    ngOnDestroy(): void {
        this._subs.unsubscribe();
    }
}

Track by 函数

操作 DOM 是一项昂贵的任务。默认情况下,`ngFor` 执行简单的相等性检查来查看项目是否已更改。`ngFor` 指令提供了 `trackBy` 函数,它决定了 Angular 如何跟踪集合内对象的变化,以便 ngFor 可以执行高效的更新。

当集合内的对象发生变化时,指令必须重绘正确的 DOM 元素。由于并非所有 DOM 节点都受到影响,因此只有已更改的元素才会被重新渲染。

在 HTML 中

<li *ngFor="let item of strategyItem; trackBy: trackByFn">{{ item }}</li>

在 TypeScript 中

public trackByFn(index, item) {
    if (!item) return null;
    return item.id;
}

Web Workers

虽然您不会看到很多应用程序实现 web workers,但它们可以成为在后台线程中运行 CPU 密集型任务的有用资产,而不会阻塞应用程序的主线程或冻结 UI。

然而,在您的 Angular 应用中实现 web workers 之前,请考虑两个限制

  • 某些环境或平台(例如 @angular/platform-server)不支持 web workers。

  • Angular CLI 不支持通过 @angular/platform-webworker 在 web worker 中运行 Angular 本身。

寻找优化点的运维技巧

分析

现代浏览器提供性能分析工具来帮助识别运行缓慢的代码。您可以使用一个名为 Webpack Bundle Analyzer 的模块来进一步扩展您的工具箱,它允许您可视化由 Webpack 生成的文件。每个文件都由一个矩形表示,其大小与文件大小成正比。

Webpack Bundle Analyzer 将帮助您识别包含在包中的模块、占用更多空间的模块以及被错误添加的模块。Bundle Analyzer 还会深入到压缩后的包中,以发现它们的真实大小。

Lighthouse 是一个内置的 Chrome 开发者工具,用于识别和修复影响您网站性能、可访问性和用户体验的常见问题。

Lighthouse 允许您可视化有关性能、渐进式 Web 应用、最佳实践、SEO 和多平台 Web 应用的不同类别的问题。当您运行 Lighthouse 时,它会执行您的 Web 应用程序并开始收集指标。最终的报告按类别提供详细的性能分数,并为每个检测到的问题提出解决方案

Angular CLI Budgets 工具

大型 JavaScript 文件会减慢应用程序下载速度并延迟用户交互。这就是为什么一些 Angular 优化技术——包括代码压缩、AOT 编译和摇树优化——都专注于减小应用程序包的最终大小。

在开发过程中,您很可能会添加库来扩展应用程序功能,但这会导致生产文件变大。您希望将包的大小控制在一定范围内,使其不超过某个合理的预定义限制。Angular CLI Budgets 是一项功能,它允许您为应用程序包定义所需的最大大小,并在这些阈值超过其限制时向您发出警告。

您的 Angular 应用优化清单

正如我们所见,这里讨论的优化技术用不同的解决方案处理不同的问题,并非所有技巧都会对您的应用程序性能产生相同的影响。

这份 Angular 优化技巧列表很可能会为微调您的应用带来收益,利用这些功能将有助于您的 Angular 应用变得更小、更快、响应更灵敏。

© . All rights reserved.