如何提高 Angular 应用程序的性能

已发表: 2020-04-10

在谈论最伟大的前端框架时,不可能不提到 Angular。 但是,它需要程序员付出很多努力来学习和明智地使用它。 不幸的是,没有 Angular 经验的开发人员可能会以低效的方式使用它的某些功能。

作为前端开发人员,您始终需要处理的许多事情之一就是应用程序的性能。 我过去的大部分项目都集中在不断扩展和开发的大型企业应用程序上。 前端框架在这里将非常有用,但正确合理地使用它们很重要。

我准备了一份最流行的性能提升策略和技巧的快速列表,它们可以帮助您立即提高 Angular 应用程序的性能。 请记住,这里的所有提示都适用于版本 8 中的 Angular。

ChangeDetectionStrategy 和 ChangeDetectorRef

变更检测 (CD)是 Angular 用于检测数据变更并自动对其做出反应的机制。 我们可以列出标准应用程序状态更改的基本类型:

  • 活动
  • HTTP 请求
  • 计时器

这些是异步交互。 问题是:Angular 怎么知道发生了一些交互(例如点击、间隔、http 请求)并且需要更新应用程序状态?

答案是ngZone ,它基本上是一个旨在跟踪异步交互的复杂系统。 如果所有操作都由 ngZone 注册,Angular 就知道何时对某些更改做出反应。 但它不知道究竟发生了什么变化,并启动了变更检测机制,该机制以一阶深度检查所有组件。

Angular 应用程序中的每个组件都有自己的变更检测器,它定义了在启动变更检测时该组件应该如何操作——例如,如果需要重新渲染组件的 DOM(这是一项相当昂贵的操作)。 当 Angular 启动变更检测时,每个组件都会被检查,并且默认情况下它的视图(DOM)可能会被重新渲染。

我们可以通过使用 ChangeDetectionStrategy.OnPush 来避免这种情况:

 @零件({
  选择器:'foobar',
  templateUrl: './foobar.component.html',
  styleUrls: ['./foobar.component.scss'],
  changeDetection:ChangeDetectionStrategy.OnPush
})

正如您在上面的示例代码中看到的,我们必须向组件的装饰器添加一个附加参数。 但是这种新的变化检测策略是如何真正起作用的呢?

该策略告诉 Angular 一个特定的组件只依赖于它的@Inputs()。 此外,所有组件@Inputs() 将像一个不可变对象一样工作(例如,当我们只更改对象的@Input() 中的属性,而不更改引用时,不会检查该组件)。 这意味着将省略许多不必要的检查,并且应该会提高我们的应用程序性能。

只有在以下情况下才会检查具有 ChangeDetectionStrategy.OnPush 的组件:

  • @Input() 引用将改变
  • 将在组件的模板或其子模板中触发事件
  • 组件中的 Observable 会触发一个事件
  • CD将使用 ChangeDetectorRef 服务手动运行
  • 在视图上使用异步管道(异步管道标记要检查更改的组件 - 当源流将发出新值时,将检查该组件)

如果上述情况均未发生,则在特定组件内使用 ChangeDetectionStrategy.OnPush 会导致 CD 启动后不检查该组件和所有嵌套组件。

幸运的是,我们仍然可以使用 ChangeDetectorRef 服务完全控制对数据更改的反应。 我们必须记住,在我们的超时、请求、订阅回调中使用 ChangeDetectionStrategy.OnPush,如果我们真的需要,我们需要手动触发 CD:

 计数器 = 0;

构造函数(私有 changeDetectorRef:ChangeDetectorRef){}

ngOnInit() {
  设置超时(()=> {
    this.counter += 1000;
    this.changeDetectorRef.detectChanges();
  }, 1000);
}

正如我们在上面看到的,通过在超时函数中调用 this.changeDetectorRef.detectChanges(),我们可以手动强制CD 。 如果计数器以任何方式在模板内部使用,它的值将被刷新。

本节的最后一个技巧是关于永久禁用特定组件的CD 。 如果我们有一个静态组件并且我们确定它的状态不应该改变,我们可以永久禁用CD

 this.changeDetectorRef.detach()

这段代码应该在 ngAfterViewInit() 或 ngAfterViewChecked() 生命周期方法中执行,以确保我们的视图在禁用数据刷新之前正确呈现。 在CD期间将不再检查此组件,除非我们手动触发 detectChanges()。

模板中的函数调用和getter

每次更改检测器运行时,在模板中使用函数调用都会执行此函数。 同样的情况也发生在getter上。 如果可能的话,我们应该尽量避免这种情况。 在大多数情况下,我们不需要在每次CD运行期间执行组件模板中的任何函数。 取而代之的是,我们可以使用纯管道。

纯水管

纯管道是一种输出仅依赖于输入的管道,没有副作用。 幸运的是,Angular 中的所有管道默认都是纯的。

 @管道({
    名称:'大写',
    纯:真
})

但是我们为什么要避免使用带有 pure: false 的管道呢? 答案又是变更检测。 在每次 CD 运行中都会执行非纯管道,这在大多数情况下是不必要的,并且会降低我们应用程序的性能。 这是我们可以更改为纯管道的函数示例:

 转换(值:字符串,限制 = 60,省略号 = '...'){
  if (!value || value.length <= limit) {
    返回值;
  }
  const numberOfVisibleCharacters = value.substr(0, limit).lastIndexOf(' ');
  返回`${value.substr(0, numberOfVisibleCharacters)}${ellipsis}`;
}

让我们看看视图:

 <p class="description">截断(文本,30)</p>

上面的代码代表纯函数——没有副作用,输出只依赖于输入。 在这种情况下,我们可以简单地用纯管道替换这个函数:

 @管道({
  名称:'截断',
  纯:真
})
导出类 TruncatePipe 实现 PipeTransform {
  转换(值:字符串,限制 = 60,省略号 = '...'){
    ...
  }
}

最后,在这个视图中,我们得到了代码,该代码仅在文本更改时执行,独立于Change Detection

 <p class="description">{{ 文本 | 截断:30 }}</p>

延迟加载和预加载模块

当您的应用程序有多个页面时,您绝对应该考虑为项目的每个逻辑部分创建模块,尤其是延迟加载模块。 让我们考虑一下简单的 Angular 路由器代码:

 常量路线:路线= [
  {
    小路: '',
    组件:HomeComponent
  },
  {
    路径:'foo',
    loadChildren: ()=> import("./foo/foo.module").then(m => m.FooModule)
  },
  {
    路径:'酒吧',
    loadChildren: ()=> import("./bar/bar.module").then(m => m.BarModule)
  }
]
@NgModule({
  出口:[路由器模块],
  进口:[RouterModule.forRoot(routes)]
})
类 AppRoutingModule {}

在上面的示例中,我们可以看到只有当用户尝试输入特定路线(foo 或 bar)时,才会加载 fooModule 及其所有资产。 Angular 也会为这个模块生成一个单独的延迟加载会减少初始加载

我们可以做一些进一步的优化。 假设我们想让我们的应用程序在后台加载模块。 对于这种情况,我们可以使用 preloadingStrategy。 Angular 默认有两种类型的 preloadingStrategy:

  • 无预加载
  • PreloadAllModules

在上面的代码中,默认使用 NoPreloading 策略。 应用程序根据用户请求开始加载特定模块(当用户想要查看特定路线时)。 我们可以通过向路由器添加一些额外的配置来改变这一点。

 @NgModule({
  出口:[路由器模块],
  进口:[RouterModule.forRoot(路线,{
       preloadingStrategy: PreloadAllModules
  }]
})
类 AppRoutingModule {}

此配置会导致当前路由尽快显示,之后应用程序将尝试在后台加载其他模块。 聪明,不是吗? 但这还不是全部。 如果这个解决方案不符合我们的需求,我们可以简单地编写我们自己的自定义策略

假设我们只想预加载选定的模块,例如 BarModule。 我们通过为数据字段添加一个额外的字段来表明这一点。

 常量路线:路线= [
  {
    小路: '',
    组件:HomeComponent
    数据:{预加载:假}
  },
  {
    路径:'foo',
    loadChildren: ()=> import("./foo/foo.module").then(m => m.FooModule),
    数据:{预加载:假}
  },
  {
    路径:'酒吧',
    loadChildren: ()=> import("./bar/bar.module").then(m => m.BarModule),
    数据:{预加载:真}
  }
]

然后我们必须编写我们的自定义预加载函数:

 @Injectable()
导出类 CustomPreloadingStrategy 实现 PreloadingStrategy {
  preload(route: Route, load: () => Observable<any>): Observable<any> {
    返回 route.data && route.data.preload ? 加载():(空);
  }
}

并将其设置为 preloadingStrategy:

 @NgModule({
  出口:[路由器模块],
  进口:[RouterModule.forRoot(路线,{
       preloadingStrategy: CustomPreloadingStrategy
  }]
})
类 AppRoutingModule {}

目前只预加载带有参数 { data: { preload: true } } 的路由。 其余路线将像设置了 NoPreloading 一样。

自定义的 preloadingStrategy 是@Injectable(),所以这意味着如果我们需要的话,我们可以在里面注入一些服务,并以任何其他方式自定义我们的 preloadingStrategy。
使用浏览器的开发人员工具,我们可以通过使用和不使用 preloadingStrategy 的相同初始加载时间来研究性能提升。 我们还可以查看网络选项卡,以查看其他路由的块正在后台加载,而用户能够毫无延迟地看到当前页面。

追踪功能

我们可以假设大多数 Angular 应用程序使用 *ngFor 来迭代模板中列出的项目。 如果迭代列表也是可编辑的,那么 trackBy 绝对是必须的。

 <ul>
  <tr *ngFor="let product of products; trackBy: trackByProductId">
    <td>{{ product.title }}</td>
  </tr>
</ul>

trackByProductId(索引:编号,产品:产品){
  返回product.id;
}

通过使用 trackBy 函数,Angular 能够跟踪集合的哪些元素已更改(通过给定的标识符)并仅重新渲染这些特定元素。 当我们省略 trackBy 时,整个列表将被重新加载,这在 DOM 上可能是一个非常耗费资源的操作。

提前 (AOT) 编译

关于 Angular 文档:

(...) Angular 提供的组件和模板不能被浏览器直接理解,Angular 应用程序需要一个编译过程才能在浏览器中运行

Angular 提供了两种编译方式:

  • 即时(JIT) – 在运行时在浏览器中编译应用程序
  • Ahead-of-Time (AOT) – 在构建时编译应用程序

对于开发用途, JIT编译应满足开发人员的需求。 尽管如此,对于生产构建,我们绝对应该使用AOT 。 我们需要确保 angular.json 文件中的aot标志设置为 true。 这种解决方案最重要的好处包括更快的渲染、更少的异步请求、更小的框架下载大小和更高的安全性。

概括

在项目的开发和维护部分,您需要牢记应用程序的性能。 但是,自行寻找可能的解决方案可能既费时又费力。 在开发过程中检查这些常见的错误并牢记它们不仅可以帮助您立即提高 Angular 应用程序的性能,还可以帮助您避免未来的失误。

发布产品图标

使用您的 Angular 项目信任 Miquido

联系我们

想用 Miquido 开发应用程序?

考虑使用 Angular 应用程序为您的业务带来助力? 与我们联系并选择我们的 Angular 应用程序开发服务。