
本文档包含一系列实践,可以帮助我们提高角度应用的性能。 “ Angular性能清单”涵盖了不同的主题 - 从服务器端的预渲染和应用程序捆绑到运行时性能以及框架执行的更改检测的优化。
该文档分为两个主要部分:
某些实践会影响这两个类别,因此可能会有一个小的交集,但是,用例的差异以及含义将被明确提及。
与特定实践相关的大多数小节列表工具可以通过自动化开发流来使我们更加有效。
请注意,大多数实践对HTTP/1.1和HTTP/2均有效。通过指定可以应用哪个版本的协议,将提及一个例外的实践。
enableProdModeChangeDetectionStrategy.OnPush*ngFor指令trackBy选项本节中的某些工具仍在开发中,并且可能会发生变化。 Angular Core团队正在尽可能多地为我们的应用程序自动化构建过程,以便许多事情将透明地发生。
捆绑是一种标准练习,旨在减少浏览器需要执行的请求数量才能提供用户请求的申请。本质上,捆绑器作为输入的入口点列表,并产生一个或多个捆绑包。这样,浏览器可以通过仅执行几个请求来获取整个应用程序,而不是单独请求每个资源。
随着您的应用程序将所有内容捆绑到一个大捆绑包时,将再次适得其反。使用WebPack探索代码拆分技术。
由于服务器推送功能,其他HTTP请求不会引起HTTP/2的关注。
工具
使我们能够有效捆绑应用程序的工具是:
资源
这些实践使我们能够通过减少应用程序的有效载荷来最大程度地减少带宽消耗。
工具
资源
尽管我们看不到空格字符(与s Regex匹配的字符)仍然由通过网络传输的字节表示。如果我们将白色空间从模板减少到最低限度,我们将分别能够进一步删除AOT代码的捆绑包大小。
值得庆幸的是,我们不必手动执行此操作。 ComponentMetadata接口提供了属性preserveWhitespaces ,默认情况下具有false价值,默认情况下,Angular编译器将修剪空格,以进一步减少应用程序的大小。如果我们将属性设置为true Angular将保留空格。
对于我们的应用程序的最终版本,我们通常不使用Angular和/或任何第三方库提供的整个代码,甚至是我们编写的库。得益于ES2015模块的静态性质,我们能够摆脱应用程序中未引用的代码。
例子
// foo.js
export foo = ( ) => 'foo' ;
export bar = ( ) => 'bar' ;
// app.js
import { foo } from './foo' ;
console . log ( foo ( ) ) ;一旦我们树木摇晃和捆绑包app.js我们将得到:
let foo = ( ) => 'foo' ;
console . log ( foo ( ) ) ;这意味着未使用的出口bar将不会包含在最终捆绑包中。
工具
注意: GCC尚未支持export * ,这对于构建角度应用至关重要,因为“枪管”模式的大量使用。
资源
自Angular版本6发布以来,Angular团队提供了一项新功能,以允许服务摇摇欲坠,这意味着您的服务将不包含在最终捆绑包中,除非其他服务或组件使用它们。这可以通过从捆绑包中删除未使用的代码来帮助减少捆绑包大小。
您可以通过使用providedIn属性来定义使用@Injectable()装饰器时应在哪里初始化服务的位置来使您的服务摇晃。然后,您应该将其从NgModule声明的providers属性以及其导入语句中删除,如下所示。
前:
// app.module.ts
import { NgModule } from '@angular/core'
import { AppRoutingModule } from './app-routing.module'
import { AppComponent } from './app.component'
import { environment } from '../environments/environment'
import { MyService } from './app.service'
@ NgModule ( {
declarations : [
AppComponent
] ,
imports : [
...
] ,
providers : [ MyService ] ,
bootstrap : [ AppComponent ]
} )
export class AppModule { } // my-service.service.ts
import { Injectable } from '@angular/core'
@ Injectable ( )
export class MyService { }后:
// app.module.ts
import { NgModule } from '@angular/core'
import { AppRoutingModule } from './app-routing.module'
import { AppComponent } from './app.component'
import { environment } from '../environments/environment'
@ NgModule ( {
declarations : [
AppComponent
] ,
imports : [
...
] ,
providers : [ ] ,
bootstrap : [ AppComponent ]
} )
export class AppModule { } // my-service.service.ts
import { Injectable } from '@angular/core'
@ Injectable ( {
providedIn : 'root'
} )
export class MyService { }如果没有将MyService注入任何组件/服务中,则不会包含在捆绑包中。
资源
野生工具中可用的挑战(例如GCC,汇总等)是角度组件的HTML样模板,无法用其功能分析。这使他们的震惊的支持效率降低了,因为他们不确定模板中的哪些指令。 AOT编译器将Angular HTML样模板转移到JavaScript或用ES2015模块导入的JavaScript或Typescript。这样,我们就可以在捆绑期间有效地养树,并删除由角,第三方库或我们自己定义的所有未使用指令。
资源
响应的有效载荷标准实践的压缩降低带宽标准实践。通过指定标头Accept-Encoding的值,浏览器暗示了服务器在客户端计算机上可用的压缩算法。另一方面,服务器设置了响应的Content-Encoding标头的值,以便告诉浏览器已选择哪种算法来压缩响应。
工具
这里的工具不是角度特定的,并且完全取决于我们正在使用的Web/Application服务器。典型的压缩算法是:
资源
资源预取用是改善用户体验的好方法。我们可以预先提取资产(图像,样式,旨在懒惰的模块等)或数据。有不同的预取策略,但其中大多数取决于应用程序的细节。
如果目标应用程序具有具有数百个依赖关系的巨大代码库,则上面列出的实践可能无法帮助我们将捆绑包降低到合理的尺寸(合理的可能是100k或2m,它再次完全取决于业务目标)。
在这种情况下,一个好的解决方案可能是加载某些应用程序的模块。例如,假设我们正在建立一个电子商务系统。在这种情况下,我们可能希望与面向用户的UI独立加载管理面板。一旦管理员必须添加新产品,我们就需要为此提供UI。根据我们的用例/业务要求,这可能仅是“添加产品页面”或整个管理面板。
工具
假设我们具有以下路由配置:
// Bad practice
const routes : Routes = [
{ path : '' , redirectTo : '/dashboard' , pathMatch : 'full' } ,
{ path : 'dashboard' , loadChildren : ( ) => import ( './dashboard.module' ) . then ( mod => mod . DashboardModule ) } ,
{ path : 'heroes' , loadChildren : ( ) => import ( './heroes.module' ) . then ( mod => mod . HeroesModule ) }
] ;用户第一次使用URL打开应用程序时:https://example.com/他们将被重定向到/dashboard ,这将用路径dashboard触发懒惰的路线。为了渲染模块的引导组件,它将必须下载文件dashboard.module 。模块及其所有依赖项。后来,该文件需要由JavaScript VM解析并进行了评估。
在初始页面加载期间触发额外的HTTP请求并执行不必要的计算是一种不良练习,因为它会减慢初始页面渲染。考虑将默认页面路由声明为非懒惰。
缓存是另一种常见的做法,旨在利用启发式方法,即如果最近要求一种资源,可能会在不久的将来再次要求它。
对于缓存数据,我们通常使用自定义缓存机制。对于缓存静态资产,我们可以使用标准浏览器缓存或使用Cachestorage API使用标准的浏览器缓存。
为了使应用程序的感知性能更快,请使用应用程序外壳。
应用程序外壳是我们向用户显示的最小用户界面,以表明他们将很快提供应用程序。为了动态生成应用程序外壳,您可以将Angular Universal与自定义指令一起使用,该指令根据使用的渲染平台有条件地显示元素(即使用platform-server时,除了应用程序外壳之外,都会隐藏所有内容)。
工具
资源
我们可以将服务工作者视为位于浏览器中的HTTP代理。从客户端发送的所有请求首先被服务工作者拦截,可以处理或通过网络传递它们。
您可以通过运行ng add @angular/pwa将服务工作者添加到您的角度项目中
工具
资源
本节包括可以应用的实践,以便为每秒60帧(FPS)提供更顺畅的用户体验。
enableProdMode在开发模式下,Angular执行一些额外的检查,以验证执行变更检测不会导致任何绑定的任何其他更改。这样,框架确保已遵循单向数据流。
为了禁用这些更改以进行生产,请不要忘记调用enableProdMode :
import { enableProdMode } from '@angular/core' ;
if ( ENV === 'production' ) {
enableProdMode ( ) ;
}AOT不仅有助于通过摇摇欲坠来实现更高效的捆绑,还可以改善应用程序的运行时性能。 AOT的替代方案是执行运行时的即时编译(JIT),因此我们可以通过执行汇编作为构建过程的一部分来减少应用程序渲染所需的计算量。
工具
ng serve --prod Angular-CLI资源
典型的单页应用程序(SPA)中通常的问题是我们的代码通常以单个线程运行。这意味着,如果我们想通过60fps实现流畅的用户体验,则最多可以在单个帧之间执行16ms ,否则它们将下降一半。
在具有巨大组件树的复杂应用中,更改检测需要每秒进行数百万支票,这并不难开始掉落帧。得益于Angular的不可知论并与DOM体系结构解耦,因此可以在Web工作人员中运行我们的整个应用程序(包括更改检测),并使Main UI线程仅负责渲染。
工具
资源
传统水疗中心的一个重大问题是,直到可以使用最初渲染所需的整个JavaScript,就无法渲染它们。这导致了两个大问题:
服务器端渲染通过预先渲染服务器上的请求页面并在初始页面加载期间提供渲染页面的标记来解决此问题。
工具
资源
在每个异步事件上,角度在整个组件树上执行更改检测。尽管检测到更改的代码已针对内联访问进行了优化,但在复杂的应用程序中,这仍然可能是一个繁重的计算。提高变更检测性能的一种方法是不要为子树执行它,而子树不应该根据最近的动作进行更改。
ChangeDetectionStrategy.OnPush OnPush更改检测策略使我们可以禁用组件树子树的变更检测机制。通过将更改检测策略设置为任何组件中的任何组件ChangeDetectionStrategy.OnPush将仅在组件已收到不同的输入时才能执行更改检测。当Angular通过参考将其与先前的输入进行比较时,将其视为不同,并且参考检查的结果为false 。结合不变的数据结构, OnPush可以对这种“纯”组件带来巨大的性能影响。
资源
实施自定义更改检测机制的另一种方法是detach和reattach启动给定组件的变更检测器(CD)。一旦我们detach CD角将无法执行整个组件子树的检查。
通常,当用户操作或与外部服务的交互触发变更检测时,通常使用此练习。在这种情况下,我们可能需要考虑仅在需要执行更改检测的情况下将变更检测器分离并重新触及。
由于区域J. Zone.js Monkey修补了浏览器中的所有异步API,并在执行任何异步回调结束时触发更改检测。在极少数情况下,我们可能希望给定的代码在角区域的上下文之外执行,从而在不运行更改检测机制的情况下执行。在这种情况下,我们可以使用NgZone实例的runOutsideAngular 。
例子
在下面的摘要中,您可以看到使用此练习的组件的示例。当_incrementPoints方法称为_incrementPoints方法时,组件将开始每10ms颁发_points属性(默认情况下)。增量将使动画的错觉。由于在这种情况下,我们不想触发整个组件树的更改检测机制,每10ms,我们可以在角区域的上下文之外运行_incrementPoints并手动更新DOM(请参阅points设置器)。
@ Component ( {
template : '<span #label></span>'
} )
class PointAnimationComponent {
@ Input ( ) duration = 1000 ;
@ Input ( ) stepDuration = 10 ;
@ ViewChild ( 'label' ) label : ElementRef ;
@ Input ( ) set points ( val : number ) {
this . _points = val ;
if ( this . label ) {
this . label . nativeElement . innerText = this . _pipe . transform ( this . points , '1.0-0' ) ;
}
}
get points ( ) {
return this . _points ;
}
private _incrementInterval : any ;
private _points : number = 0 ;
constructor ( private _ngZone : NgZone , private _pipe : DecimalPipe ) { }
ngOnChanges ( changes : any ) {
const change = changes . points ;
if ( ! change ) {
return ;
}
if ( typeof change . previousValue !== 'number' ) {
this . points = change . currentValue ;
} else {
this . points = change . previousValue ;
this . _ngZone . runOutsideAngular ( ( ) => {
this . _incrementPoints ( change . currentValue ) ;
} ) ;
}
}
private _incrementPoints ( newVal : number ) {
const diff = newVal - this . points ;
const step = this . stepDuration * ( diff / this . duration ) ;
const initialPoints = this . points ;
this . _incrementInterval = setInterval ( ( ) => {
let nextPoints = Math . ceil ( initialPoints + diff ) ;
if ( this . points >= nextPoints ) {
this . points = initialPoints + diff ;
clearInterval ( this . _incrementInterval ) ;
} else {
this . points += step ;
}
} , this . stepDuration ) ;
}
}警告:只有在确定自己在做什么时,才非常仔细地使用此练习,因为如果不正确使用,可能会导致DOM的不一致状态。另外,请注意,上面的代码不会在网络工作人员中运行。为了使其与Webworks兼容,您需要使用Angular的渲染器来设置标签的值。
Angular使用Zone.js拦截应用程序中发生的事件并自动运行更改检测。默认情况下,当浏览器的微型箱队列为空时,这会发生这种情况,在某些情况下可能会调用冗余周期。 Angular在V9中提供了一种通过打开ngZoneEventCoalescing方式进行聚合事件变化检测的方法
platformBrowser ( )
. bootstrapModule ( AppModule , { ngZoneEventCoalescing : true } ) ;上面的配置将使用requestAnimationFrame安排更改检测,而不是插入Microtask队列,该机构的运行频率较低,并且消耗较少的计算周期。
警告: ngzoneeventCoalescing:TRUE可能会破坏在持续运行变更检测时中继的现有应用程序。
资源
作为参数, @Pipe Decorator接受以下格式字面的对象:
interface PipeMetadata {
name : string ;
pure : boolean ;
}纯标志表明管道不取决于任何全局状态,也不产生副作用。这意味着当用相同的输入调用时,管道将返回相同的输出。这样,Angular可以将管道已被调用的所有输入参数的输出缓存,并重复使用它们,以便不必在每个评估中重新计算它们。
pure性的默认值为true 。
*ngFor指令*ngFor指令用于渲染集合。
trackBy选项默认情况下*ngFor通过引用标识对象唯一性。
这意味着,当开发人员在更新项目的内容角度时断开对象的引用将其视为删除旧对象的去除和添加新对象。这种影响在破坏列表中的旧DOM节点并在其位置添加新的DOM节点。
开发人员可以为Angular提供如何识别对象唯一性的提示:自定义跟踪功能作为*ngFor指令的trackBy选项。跟踪功能需要两个参数: index和item 。 Angular使用从跟踪功能返回的值来跟踪项目身份。将特定记录的ID用作唯一键是很常见的。
例子
@ Component ( {
selector : 'yt-feed' ,
template : `
<h1>Your video feed</h1>
<yt-player *ngFor="let video of feed; trackBy: trackById" [video]="video"></yt-player>
`
} )
export class YtFeedComponent {
feed = [
{
id : 3849 , // note "id" field, we refer to it in "trackById" function
title : "Angular in 60 minutes" ,
url : "http://youtube.com/ng2-in-60-min" ,
likes : "29345"
} ,
// ...
] ;
trackById ( index , item ) {
return item . id ;
}
} 在向UI添加元素时,渲染DOM元素通常是最昂贵的操作。主要工作通常是通过将元素插入DOM并应用样式引起的。如果*ngFor呈现许多元素,则浏览器(尤其是较旧的浏览器)可能会放慢速度,并且需要更多时间来完成所有元素的渲染。这不是角度特定的。
为了减少渲染时间,请尝试以下内容:
*ngFor的DOM元素的数量。通常,不需要/未使用的DOM元素是由于一次又一次地扩展模板而引起的。重新思考其结构可能会使事情变得容易得多。ng-container资源
*ngFor的官方文件每个更改检测周期后,角度执行模板表达式。变化检测周期是由许多异步活动触发的,例如承诺决议,HTTP结果,计时事件,关键和小鼠移动。
表达式应快速完成,否则用户体验可能会拖累,尤其是在较慢的设备上。当其计算昂贵时,请考虑缓存值。
资源
实践列表将随着时间的推移而动态发展,随着新/更新的实践。如果您注意到缺少的东西或认为可以改进的任何实践都会毫不犹豫地解雇问题和/或PR。有关更多信息,请查看下面的“贡献”部分。
如果您注意到一些缺失,不完整或不正确的东西,将不胜感激。有关未包含在文档中的实践的讨论,请打开问题。
麻省理工学院