angular如何進(jìn)行性能優(yōu)化?下面本篇文章給大家深入介紹一下angular 性能優(yōu)化方案–變更檢測(cè),希望對(duì)大家有所幫助!
angular 性能優(yōu)化——變更檢測(cè)
對(duì)于前端性能指標(biāo)描述,業(yè)界都各有說(shuō)詞,總結(jié)下來(lái)都和首屏性能和頁(yè)面流暢度相關(guān), 本次將會(huì)從頁(yè)面流暢度的角度,對(duì)頁(yè)面交互性能優(yōu)化進(jìn)行分析?!鞠嚓P(guān)教程推薦:《angular教程》】
什么是頁(yè)面流暢度?
頁(yè)面流暢度是通過(guò)幀率 FPS(Frames Per Second – 每秒傳輸幀數(shù))判定的,一般主流的瀏覽器屏幕刷新率都在 60Hz(每秒刷新60次),最優(yōu)的幀率在 60 FPS,幀率越高,頁(yè)面就越流暢,60Hz意味著每隔16.6ms會(huì)刷新一次顯示屏,也就是每一次渲染頁(yè)面需要在16.6ms內(nèi)完成,否則就會(huì)導(dǎo)致頁(yè)面失幀,出現(xiàn)卡頓現(xiàn)象。根因在于:瀏覽器中的 JavaScript 執(zhí)行和頁(yè)面渲染會(huì)相互阻塞
。
在 Chrome 的 devtools 中我們可以執(zhí)行 Cmd+Shift+P 輸入 show fps 來(lái)快速打開 fps 面板,如下圖所示:
通過(guò)觀察 FPS 面板,我們可以很方便的對(duì)當(dāng)前頁(yè)面的流暢度進(jìn)行監(jiān)控
1 影響頁(yè)面性能的因素
頁(yè)面交互是否流暢,在于頁(yè)面響應(yīng)是否流暢,而頁(yè)面響應(yīng)其本質(zhì)上就是把頁(yè)面狀態(tài)的變更重新渲染到頁(yè)面上的過(guò)程。
頁(yè)面響應(yīng)過(guò)程大致如下:
一般情況Event Handler事件處理邏輯不會(huì)消耗太多時(shí)間,所以影響angular性能的因素主要在于異步事件觸發(fā)
和變更檢測(cè)
。 一般情況Event Handler事件處理邏輯不會(huì)消耗太多時(shí)間,所以影響angular性能的因素主要在于異步事件觸發(fā)和變更檢測(cè)。
對(duì)angular來(lái)說(shuō),頁(yè)面渲染的過(guò)程就是變更檢測(cè)的過(guò)程,可以理解為angular的變更檢測(cè)要在16.6ms內(nèi)完成才不會(huì)導(dǎo)致頁(yè)面失幀、卡頓。
可以從以下三方面優(yōu)化頁(yè)面響應(yīng)的性能。
(1)對(duì)于觸發(fā)事件階段,可以減少異步事件的觸發(fā),來(lái)減少整體的變更檢測(cè)次數(shù)和重新渲染;
(2)對(duì)于 Event Handler 執(zhí)行邏輯階段,可以通過(guò)優(yōu)化復(fù)雜代碼邏輯來(lái)減少執(zhí)行時(shí)間;
(3)對(duì)于 Change Detection 檢測(cè)數(shù)據(jù)綁定并更新 DOM 階段,可以減少變更檢測(cè)和模板數(shù)據(jù)的計(jì)算次數(shù)來(lái)減少渲染時(shí)間;
對(duì)于(2)Event Handler要具體問(wèn)題具體分析,不做討論,主要針對(duì)(1)(3)進(jìn)行優(yōu)化
2 優(yōu)化方案
2.1 減少異步事件觸發(fā)
Angular在默認(rèn)變更檢測(cè)模式下,異步事件會(huì)觸發(fā)全局的變更檢測(cè),因此,減少異步事件的觸發(fā)會(huì)大大的提升angular的性能。
異步事件包括Macro Task(宏任務(wù))事件和Micro Task微任務(wù)事件
對(duì)異步事件的優(yōu)化主要是針對(duì)document的監(jiān)聽事件。比如document上的click、mouseup、mousemove…等監(jiān)聽事件。
監(jiān)聽事件場(chǎng)景:
Renderer2.listen(document, …)
fromEvent(document,…)
document.addEventListener(…)
dom監(jiān)聽事件,在不需要觸發(fā)的時(shí)候一定要移除。
舉例:[pop]提示框指令
使用場(chǎng)景:表格列篩選,點(diǎn)擊圖標(biāo)以外的地方,或者頁(yè)面滾動(dòng),列篩選彈框隱藏
以前的做法是直接在pop指令里監(jiān)聽document的click事件和scroll事件,這樣有個(gè)弊端就是提示框未顯示,但依然存在監(jiān)聽事件,很不合理。
合理的解決方案:當(dāng)提示框顯示的時(shí)候才去監(jiān)聽click和scroll事件,隱藏的時(shí)候就移除監(jiān)聽事件。
對(duì)于頻繁觸發(fā)的dom監(jiān)聽事件,可以使用rjx的操作符對(duì)事件進(jìn)行優(yōu)化。詳情參考Rjx操作符。RxJS Marbles。
2.2 變更檢測(cè)
什么是變更檢測(cè)?
要理解變更檢測(cè),我們可以從變更檢測(cè)的目標(biāo)尋找答案。angular變更檢測(cè)目標(biāo),是讓模型(TypeScript代碼)與模板(HTML)保持同步。因此,變更檢測(cè)可以理解為:檢測(cè)模型變更的同時(shí),更新模板( DOM ) 。
變更檢測(cè)流程是什么?
通過(guò)在組件樹中按照自頂向下的順序執(zhí)行變更檢測(cè),也就是先對(duì)父組件執(zhí)行變更檢測(cè),再對(duì)子組件進(jìn)行變更檢測(cè)。首先檢查父組件的數(shù)據(jù)變更,然后更新父組件模板,在更新模板的時(shí)候遇到子組件,會(huì)去更新子組件上綁定的值,然后進(jìn)入子組件,看@Input輸入值的索引是否改變,如果改變就將該子組件標(biāo)記為dirty,也就是后續(xù)需要變更檢測(cè)的,標(biāo)記完子組件之后,繼續(xù)更新父組件中子組件后面的模板,父組件模板全部更新完之后再去對(duì)子組件做變更檢測(cè)。
2.2.1 angular變更檢測(cè)原理
在默認(rèn)變更檢測(cè)default模式下,異步事件觸發(fā)Angular的變更檢測(cè)的原理是 angular通過(guò)使用Zone.js處理異步事件時(shí)調(diào)用了ApplicationRef 的tick()方法從根組件到子組件執(zhí)行變更檢測(cè)。 Angular 應(yīng)用初始化過(guò)程中,實(shí)例化了一個(gè)zone (NgZone),然后將所有邏輯都跑在該對(duì)象的 _inner 對(duì)象中。
Zone.js實(shí)現(xiàn)了以下幾個(gè)類:
- Zone類,JavaScript 事件的執(zhí)行環(huán)境,和線程一樣,它們可以帶一些數(shù)據(jù),并且可能擁有父子 zone。
- ZoneTask類,包裝后的異步事件,這些 task 有三種子類:
-
- MicroTask,由 Promise 創(chuàng)建。
- MacroTask,由 setTimeout 等創(chuàng)建。
- EventTask,由 addEventListener 等創(chuàng)建,比如dom事件。
- ZoneSpec對(duì)象,創(chuàng)建一個(gè) ngZone 時(shí)給它提供的參數(shù),有三個(gè)可以觸發(fā)檢測(cè)的鉤子:
-
- onInvoke,調(diào)用某個(gè)回調(diào)函數(shù)時(shí)觸發(fā)的鉤子。
- onInvokeTask,ZoneTask 被觸發(fā)時(shí)觸發(fā)的鉤子,比如 setTimeout 到時(shí)。
- onHasTask,檢測(cè)到有或無(wú) ZoneTask 時(shí)觸發(fā)的鉤子(即對(duì)第一個(gè) schedule 的 zone 和最后一個(gè) invoke 或 cancel 的 task 觸發(fā))。
- ZoneDelegate類,負(fù)責(zé)調(diào)用鉤子。
檢測(cè)過(guò)程原理大概如下:
用戶操作觸發(fā)異步事件(比如:dom事件,接口請(qǐng)求…)
=> ZoneTask類處理事件。invokeTask()函數(shù)中調(diào)用zone的runTask()方法,在runTask方法中,zone通過(guò)_zoneDelegate實(shí)例屬性,調(diào)用ZoneSpec的鉤子
=> ZoneSpec的三個(gè)鉤子(onInvokeTask、onInvoke、onHasTask)鉤子里通過(guò)checkStable()函數(shù)觸發(fā)zone.onMicrotaskEmpty.emit(null)通知
=> 根組件監(jiān)聽onMicrotaskEmpty后調(diào)用tick(),tick方法中調(diào)用 detectChanges()
從根組件開始檢測(cè)
=> ··· refreshView()
調(diào)用executeTemplate()
,executeTemplate
方法中調(diào)用templateFn()
更新模板、子組件綁定的值(這時(shí)候會(huì)去檢測(cè)子組件的
@Input()輸入引用是否改變,如果有改變,會(huì)將子組件標(biāo)記為
Dirty,也就是該子組件需要變更檢測(cè)
)
詳細(xì)變更檢測(cè)原理流程圖:
簡(jiǎn)化流程:
觸發(fā)異步事件
=> ZoneTask處理事件
=> ZoneDelegate 調(diào)用ZoneSpec的鉤子觸發(fā)onMicrotaskEmpty通知
=> 根組件收到onMicrotaskEmpty通知,執(zhí)行tick(),開始檢測(cè)并更新dom
由以上代碼可知,當(dāng)微任務(wù)為空的時(shí)候才會(huì)觸發(fā)變更檢測(cè)
。
簡(jiǎn)略變更檢測(cè)原理流程圖:
Angular 源碼解析 Zone.js參考blog。
2.2.2 變更檢測(cè)優(yōu)化方案
1 )使用OnPush 模式
原理:減少1次變更檢測(cè)的耗時(shí)。
OnPush模式與Default模式的區(qū)別在于:dom監(jiān)聽事件、timer事件、promise都不會(huì)觸發(fā)變更檢測(cè)。Default模式的組件狀態(tài)始終為CheckAlways,表示組件每次檢測(cè)周期都要檢測(cè)。
OnPush模式下:以下情況會(huì)觸發(fā)變更檢測(cè)
S1、組件的@Input引用發(fā)生變化。
S2、組件的DOM綁定的事件,包括它子組件的DOM綁定的事件,比如 click、submit、mouse down。@HostListener()
注意:
通過(guò)renderer2.listen()監(jiān)聽的dom事件不會(huì)觸發(fā)變更檢測(cè)
通過(guò)dom.addEventListener()原生監(jiān)聽方式也不會(huì)觸發(fā)變更檢測(cè)
S3、Observable 訂閱事件,同時(shí)設(shè)置 Async pipe。
S4、利用以下方式手動(dòng)觸發(fā)變化檢測(cè):
ChangeDetectorRef.detectChanges():觸發(fā)當(dāng)前組件和非OnPush子組件的變更檢測(cè)。
ChangeDetectorRef.markForCheck():將當(dāng)前視圖及其所有的祖先標(biāo)記為臟,下次檢測(cè)周期時(shí)候會(huì)觸發(fā)檢測(cè)。
ApplicationRef.tick():不會(huì)觸發(fā)變更檢測(cè)
2 )使用NgZone.runOutsideAngular()
原理:減少變更檢測(cè)次數(shù)
將全局dom事件監(jiān)聽寫在NgZone.runOutsideAngular()方法的回調(diào)里面,dom事件將不會(huì)觸發(fā)angular的變更檢測(cè)。如果當(dāng)前組件未更新,可以在回調(diào)函數(shù)里執(zhí)行ChangeDetectorRef的detectChanges()鉤子來(lái)手動(dòng)觸發(fā)當(dāng)前組件的變更檢測(cè)。
舉例:app-icon-react動(dòng)態(tài)圖標(biāo)組件
2.2.3 調(diào)試方式
方式1:可以在瀏覽器控制臺(tái),使用Angular DevTools插件查看某一次dom事件,angular的檢測(cè)情況:
方式2:可以在控制臺(tái)直接輸入:ng.profiler.timeChangeDetection()查看檢測(cè)時(shí)間,這種方式可查看全局的檢測(cè)時(shí)間。參考博客 Profiling Angular Change Detection
2.3 模板(HTML)優(yōu)化
2.3.1 減少DOM渲染:ngFor加trackBy
使用 *ngFor 的 trackBy 屬性,Angular 只更改和重新渲染已更改的條目,而不必重新加載整個(gè)條目列表。
比如:表格排序場(chǎng)景。ngFor如果加了trackBy,表格渲染的時(shí)候只會(huì)移動(dòng)行dom元素,如果不加trackBy,會(huì)先刪除現(xiàn)有的表格dom元素,再新增行dom元素。顯然只移動(dòng)dom元素性能會(huì)好很多。
2.3.2 模板表達(dá)式中不要使用函數(shù)
不要在Angular 模板表達(dá)式中使用函數(shù)調(diào)用,可以用管道pipe代替,也可以通過(guò)手動(dòng)計(jì)算后用一個(gè)變量代替。模板中使用函數(shù),不管值有沒(méi)有改變,每次變更檢測(cè)的時(shí)候都會(huì)執(zhí)行函數(shù),會(huì)影響性能。
模板中使用函數(shù)的場(chǎng)景:
2.3.3 減少ngFor的使用
使用ngFor,數(shù)據(jù)量大的時(shí)候會(huì)影響性能。
舉例:
使用ngFor:
不使用ngFor:性能提升10倍左右