
本文檔包含一系列實踐,可以幫助我們提高角度應用的性能。 “ 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。有關更多信息,請查看下面的“貢獻”部分。
如果您注意到一些缺失,不完整或不正確的東西,將不勝感激。有關未包含在文檔中的實踐的討論,請打開問題。
麻省理工學院