使用 DevTools 對(duì) Angular 前端應(yīng)用性能分析優(yōu)化
使用 lighthouse 評(píng)分
以南寧IT派[www.nnitpai.com]為例記錄分析優(yōu)化過(guò)程,使用 Devtools lighthouse 對(duì)首頁(yè)進(jìn)行綜合的評(píng)分:

性能評(píng)分勉強(qiáng)及格差強(qiáng)人意,切換到 performance 性能選項(xiàng)卡:

記錄的同時(shí),可以依次滾動(dòng)頁(yè)面到底部,暫??纯捶治鼋Y(jié)果:

發(fā)現(xiàn)一推很深的函數(shù)調(diào)用,放大具體看看:(記得要用本地開發(fā)環(huán)境來(lái)查看,這樣可以方便看未編譯版本中具體的組件或者函數(shù))

發(fā)現(xiàn)大部分的深度調(diào)用都與這個(gè) MenuComponent 組件有關(guān),不斷的在調(diào)用刷新,可以看到一個(gè) executeTemplate 這個(gè)函數(shù),這個(gè)是angular對(duì)模板里面的變量或者函數(shù)執(zhí)行計(jì)算值,已檢測(cè)是否有變化,相應(yīng)進(jìn)行渲染。
看看 MenuComponent 組件模板關(guān)鍵部分:
<ng-container?*ngIf="content">
??<header?*ngIf="screen.eq('gt-sm')"?class="header"?#header>
????//
??header>
??<div?class="drawer">
????<mat-toolbar?*ngIf="screen.eq('lt-md')">
??????//
????mat-toolbar>
????<div?[hidden]="!(screen.eq('lt-md')?&&?isDrawer)">
??????//
????div>
??div>
ng-container>
里面有個(gè)方法調(diào)用screen.eq()在eq方法打個(gè)log看看日志:

只要頁(yè)面有事件發(fā)生,比如dom的更改,這個(gè)log一直飆升,導(dǎo)致嚴(yán)重的性能問(wèn)題,問(wèn)題應(yīng)該就是在這里,為什么會(huì)被反復(fù)執(zhí)行?
簡(jiǎn)單了解 Angular 的默認(rèn)更新檢測(cè)機(jī)制
有幾種情況會(huì)觸發(fā) Angular 檢測(cè):
- 用戶的輸出操作,點(diǎn)擊、提交、滾動(dòng)等等
- http 請(qǐng)求
- 定時(shí)事件,setTimeout\setInterval
變更檢測(cè)機(jī)制是 Angular 內(nèi)置的框架功能,可確保組件的數(shù)據(jù)與HTML模板視圖進(jìn)行自動(dòng)同步,對(duì)于模板中使用的表達(dá)式,它將當(dāng)前值與先前值進(jìn)行比較,如果值不同則 isChanged,然后進(jìn)行更新,組件的檢測(cè)機(jī)制:
- Default: 默認(rèn)的檢測(cè)機(jī)制,比較事件發(fā)生前后的模板表達(dá)式值來(lái)決定是否更新視圖,只要有更新,會(huì)更新整個(gè)數(shù)組樹。
- OnPush: 檢測(cè)組件的Input輸入或者異步管道訂閱的數(shù)據(jù)發(fā)生變化時(shí)才會(huì)更新;
尋找問(wèn)題的原因
當(dāng)頁(yè)面滾到在背景視頻的時(shí)候,log的輸出特別的密集,仔細(xì)觀察這個(gè)組件有DOM數(shù)據(jù)的刷新:

這應(yīng)該是使用了定時(shí)器更新了當(dāng)前的播放時(shí)間,導(dǎo)致 angular 不斷觸發(fā)監(jiān)測(cè)機(jī)制。
解決問(wèn)題
把組件默認(rèn)監(jiān)測(cè)機(jī)制更改為OnPush
從而可以忽略外部其他組件的變更周期,當(dāng)輸入屬性發(fā)生變化時(shí)才會(huì)觸發(fā)。
import?{
??Component,
??OnInit,
??Input,
??ChangeDetectionStrategy,
}?from?'@angular/core';
@Component({
??selector:?'app-menu',
??templateUrl:?'./menu.component.html',
??styleUrls:?['./menu.component.scss'],
??changeDetection:?ChangeDetectionStrategy.OnPush,
})
手動(dòng)更新檢查
screen.eq('xs') 方法是用來(lái)檢測(cè)當(dāng)前屏幕的媒體查詢,根據(jù)屏幕尺寸做出顯示邏輯,使用 OnPush方式時(shí),模板表達(dá)式不會(huì)再隨著外部的檢測(cè)周期影響到,當(dāng)頁(yè)面寬度發(fā)生變化時(shí),我們就需要手動(dòng)的去告訴組件更新檢測(cè)。
新建一個(gè)媒體查詢的服務(wù),在組件里面訂閱這個(gè)可觀察對(duì)象,這個(gè)觀察對(duì)象會(huì)在瀏覽器寬度發(fā)生變化時(shí)推送當(dāng)前窗口的媒體查詢值:
mqAlias$():?Observable<string[]>?{
????return?this.mediaObserver.asObservable().pipe(
??????distinctUntilChanged(
????????(x:?MediaChange[],?y:?MediaChange[])?=>
??????????this.getAlias(x)?===?this.getAlias(y)
??????),
??????map((change:?any)?=>?{
????????return?change.map((item:?any)?=>?{
??????????return?item.mqAlias;
????????});
??????})
????);
??}
并在組件中引用
@Component({
??selector:?'app-menu-item',
??templateUrl:?'./menu-item.component.html',
??styleUrls:?['./menu-item.component.scss'],
??changeDetection:?ChangeDetectionStrategy.OnPush,
})
export?class?MenuItemComponent?implements?OnInit?{
??@Input()?content:?any;
??@Input()?mobileMenu:?any;
??showXs:?boolean;
??constructor(
????private?screen:?ScreenState,
????private?screenService:?ScreenService,
????private?cd:?ChangeDetectorRef
??)?{}
??ngOnInit():?void?{
????if?(this.screenService.isPlatformBrowser())?{
??????this.screen.mqAlias$().subscribe((alia)?=>?{
????????this.showXs?=?alia.includes('xs');
????????this.cd.detectChanges();
??????});
????}
??}
}

總結(jié)
評(píng)分提升還是很明顯的,對(duì)于后面組件的開發(fā)提供了很好的最佳實(shí)踐,多理解熟悉 Angular 的內(nèi)部運(yùn)行機(jī)制,為項(xiàng)目開發(fā)帶來(lái)更好的效益。
- 要善于使用 lighthouse 進(jìn)行檢測(cè)評(píng)分,針對(duì)不同問(wèn)題具體分析;
- 多使用 Devtools 分析,查找問(wèn)題的入口;
- 不要在模板中使用函數(shù)或者getter,當(dāng)有大量變更時(shí),會(huì)存在很多的性能隱患;
接下來(lái),會(huì)繼續(xù)針對(duì)這個(gè)案例繼續(xù)分析,使項(xiàng)目的 lighthouse 評(píng)分更加友好,提供給各位前端開發(fā)一些借鑒和交流。
