Angular的14种优化策略
简介
在这篇文章中,我们将去讨论如何让你的angular应用变得更小更快的14种方式:
- 启用生产模式
- AOT
- Minification
- 避免视图中的函数调用和 getter
- 纯管道
- 懒加载module
- 代码拆分
- onpush变更检测
- 异步管道
- ngDoCheck
- 函数追踪
- Zone.js
- 取消订阅 observables
- web workers
构建和部署优化
生产优化
在部署您的应用程序之前,请确保您创建了一个生产版本。此模式执行许多在开发构建中不可用的重要优化,包括AOT编译、minification和tree shaking。
JIT 和 AOT
Angular提供了两种编译模型:JIT (just-in-time),它在运行时编译应用;AOT (ahead-of-time),它在构建时编译应用。默认情况下,开发编译使用AOT编译。 AOT 在构建时进行编译,仅生成编译后的模板,并从部署捆绑包中删除 Angular 编译器,从而将您的应用程序负载减少大约 1MB(大约是 Angular 编译器的大小)。
Minification
我们的 JavaScript 代码中的许多字符,包括空格、换行符、注释和块定界符,仅用于可读性和视觉目的。它们不是代码正确运行所必需的。Minification删除了这些字符,简化了名称,并忽略了无法访问的代码。通过缩小代码,您可以加快页面下载和执行时间。 现在,比较下缩小前和缩小后的代码: 缩小前:
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"});
构建优化
Build Optimizer由Angular团队创建,是一个进一步优化Angular Webpack构建的工具。它确定了可以在构建时删除而不会产生副作用的代码。例如,Build Optimizer可以从AOT构建中删除@Component这样的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提供了惰性加载,这是一种按需加载页面的简单方法。使用惰性加载,只有当用户导航到特定模块的路由时,模块才会被加载。Angular团队在路由器中内置了这个特性,所有这些都是在底层为你完成的。因此,延迟加载使用起来很简单。
然而,有一个事实被忽略了,利用惰性加载的必要条件是:它需要更多的模块。我们将应用程序划分为模块的一个好处是能够按需加载模块。因此假如你写了3万行代码,它们都在一个模块中,你就不能利用惰性加载功能,你的应用程序可能会越来越慢。
没有懒加载的路由:
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 } from '@angular/core';
@Component({
selector: 'app-user-list',
templateUrl: './user-list.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserListComponent {
}
ngDoCheck生命周期钩子
每次运行变更检测时,Angular 都会调用 ngDoCheck。因此,ngDoCheck 是组件中添加计算密集型或缓慢的自定义逻辑或检测 Angular 会忽略的更改的完美位置。您可以将复杂的计算移动到 ngDoCheck 生命周期挂钩中,并在您的视图中引用计算值。请记住,缓存复杂的计算结果会产生更好的性能。但由于该生命周期调用频繁的缘故,我们不应在该生命周期内使用大量复杂且不能缓存的计算。
异步管道
在使用可观察对象时,调用subscribe方法,但忘记调用unsubscribe,随后会导致内存泄漏。内存泄漏是异步管道成为您的朋友的原因:它为您处理所有的清理工作。它不仅为您订阅,而且还在您关闭组件时负责取消订阅,并且在每次更新时调用markForCheck。因此,异步管道是OnPush更改检测策略的完美匹配。
async管道允许你直接在视图中使用RxJS的可观察对象。从下面的代码中可以看到,每当异步管道更新一个值时,它都会自动为您调用markForCheck。因此,您只注入数据服务,而不将实际值放在组件中的字段中。相反,您可以添加一个对可观察对象的引用。现在,当有更新发送到这个可观察对象时,你的组件也会检测到变化:
<span>Wait for it... {{ greeting | async }}</span>
避免视图中的函数调用和 getter
当绑定到一个对象时,Angular会非常快速地在模型属性中执行变更验证,因为它不需要执行任何函数。然而,当你绑定到一个函数或JavaScript getter时,Angular必须运行你的函数来检查值是否发生了变化。在某些情况下,Angular会频繁地执行变更检测,导致应用出现严重的性能问题。如果可以避免这种情况的话,永远不要在Angular模板表达式中绑定函数或getter。相反,使用纯管道可以让Angular在值不变的情况下高效地跳过管道执行。你也可以手动计算组件控制器中的值,并在需要时重新计算它们。
纯管道
有时,您需要在视图中调用函数,但在许多情况下,您可以使用管道代替。
import {Pipe, PipeTransform} from '@angular/core';
@Pipe({name: 'repeat'})
export class RepeatPipe implements PipeTransform {
transform(value: any, times: number) {
return value.repeat(times);
}
}
Zone.js
有时候,你的视图没有更新,后来你会发现,当模型被异步函数(比如setInterval、setTimeout、鼠标事件或promise拒绝)更新时,Angular不会检测到变化。为了解决这个问题,人们求助于AngularJS替代这些异步函数,但他们仍然必须以编程化的方式更新视图。
Angular2引入了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 区域之外运行循环如何不会导致 UI 在每个 setTimeout 周期后刷新。
Unsubscribing Observables
取消订阅很简单:存储订阅,然后使用ngOnDestroy生命周期钩子函数在订阅对象上调用取消订阅。
幸运的是,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();
}
}
函数追踪
操作DOM是一项代价高昂的任务。默认情况下,ngFor会执行一个简单的相等性检查,看看项目是否发生了变化。ngFor指令提供了trackBy函数,它决定了Angular如何跟踪集合中对象的变化,以便ngFor能够执行有效的更新。
当对象在集合内改变时,指令必须重绘正确的DOM元素。由于并非所有DOM节点都受到影响,因此只会重新呈现已更改的元素。
<li *ngFor="let item of strategyItem; trackBy: trackByFn">\{{ item }}</li>
public trackByFn(index, item) {
if (!item) return null;
return item.id;
}
web workers
虽然没有看到很多应用程序实现了 Web Worker,但它们可以成为在后台线程中运行 CPU 密集型任务的有用资产,而不会阻塞应用程序的主线程或冻结 UI。
但是,在你的Angular应用中实现web worker之前,要考虑两个限制:
- 一些环境或平台(如@angular/platform-server)不支持web worker。
- Angular CLI不支持通过@angular/platform-webworker在webworker中运行Angular本身。
寻找优化的操作技巧
现代浏览器提供性能分析工具来帮助识别运行缓慢的代码。你可以使用一个叫做Webpack Bundle Analyzer的模块进一步扩展你的工具带,它允许你可视化Webpack生成的文件。每个文件都由一个矩形表示,其大小与文件大小相关。
Webpack Bundle Analyzer将帮助您识别包中包含的模块,占用更多空间的模块,以及错误添加的模块。Bundle Analyzer还可以进入缩小的Bundle,以发现它们的实际大小。
Lighthouse 用作内置的 Chrome 开发工具,用于识别和修复影响网站性能、可访问性和用户体验的常见问题。
转载自:https://juejin.cn/post/7213307113111994427