作者:ouven
https://my.oschina.net/zhangstephen/blog/1601380
前端性能優(yōu)化是一個很寬泛的概念,本書前面的部分也多多少少提到一些前端優(yōu)化方法,這也是我們一直在關(guān)注的一件重要事情。配合各種方式、手段、輔助系統(tǒng),前端優(yōu)化的最終目的都是提升用戶體驗(yàn),改善頁面性能,我們常常竭盡全力進(jìn)行前端頁面優(yōu)化,但卻忽略了這樣做的效果和意義。先不急于探究前端優(yōu)化具體可以怎樣去做,先看看什么是前端性能,應(yīng)該怎樣去了解和評價前端頁面的性能。
通常前端性能可以認(rèn)為是用戶獲取所需要頁面數(shù)據(jù)或執(zhí)行某個頁面動作的一個實(shí)時性指標(biāo),一般以用戶希望獲取數(shù)據(jù)的操作到用戶實(shí)際獲得數(shù)據(jù)的時間間隔來衡量。例如用戶希望獲取數(shù)據(jù)的操作是打開某個頁面,那么這個操作的前端性能就可以用該用戶操作開始到屏幕展示頁面內(nèi)容給用戶的這段時間間隔來評判。用戶的等待延時可以分成兩部分:可控等待延時和不可控等待延時。可控等待延時可以理解為能通過技術(shù)手段和優(yōu)化來改進(jìn)縮短的部分,例如減小圖片大小讓請求加載更快、減少HTTP請求數(shù)等。不可控等待延時則是不能或很難通過前后端技術(shù)手段來改進(jìn)優(yōu)化的,例如鼠標(biāo)點(diǎn)擊延時、CPU計(jì)算時間延時、ISP(Internet Service Provider,互聯(lián)網(wǎng)服務(wù)提供商)網(wǎng)絡(luò)傳輸延時等。所以要知道的是,前端中的所有優(yōu)化都是針對可控等待延時這部分來進(jìn)行的,下面來了解一下如何獲取和評價一個頁面的具體性能。
前端性能測試
獲取和衡量一個頁面的性能,主要可以通過以下幾個方面:Performance Timing API、Profile工具、頁面埋點(diǎn)計(jì)時、資源加載時序圖分析。
一、Performance Timing API
Performance Timing API是一個支持Internet Explorer 9以上版本及WebKit內(nèi)核瀏覽器中用于記錄頁面加載和解析過程中關(guān)鍵時間點(diǎn)的機(jī)制,它可以詳細(xì)記錄每個頁面資源從開始加載到解析完成這一過程中具體操作發(fā)生的時間點(diǎn),這樣根據(jù)開始和結(jié)束時間戳就可以計(jì)算出這個過程所花的時間了。
圖1為W3C標(biāo)準(zhǔn)中Performance Timing資源加載和解析過程記錄各個關(guān)鍵點(diǎn)的示意圖,瀏覽器中加載和解析一個HTML文件的詳細(xì)過程先后經(jīng)歷unload、redirect、App Cache、DNS、TCP、Request、Response、Processing、onload幾個階段,每個過程開始和結(jié)束的關(guān)鍵時間戳瀏覽器已經(jīng)使用performance.timing來記錄了,所以根據(jù)這個記錄并結(jié)合簡單的計(jì)算,我們就可以得到頁面中每個過程所消耗的時間。
圖1 performance API關(guān)鍵時間點(diǎn)記錄
function?performanceTest()?{
????let?timing?=?performance.timing,
????readyStart?=?timing.fetchStart?-?timing.navigationStart,
????redirectTime?=?timing.redirectEnd?-?timing.redirectStart,
????appcacheTime?=?timing.domainLookupStart?-?timing.fetchStart,
????unloadEventTime?=?timing.unloadEventEnd?-?timing.unloadEventStart,
????lookupDomainTime?=?timing.domainLookupEnd?-?timing.domainLookupStart,
????connectTime?=?timing.connectEnd?-?timing.connectStart,
????requestTime?=?timing.responseEnd?-?timing.requestStart,
????initDomTreeTime?=?timing.domInteractive?-?timing.responseEnd,
????domReadyTime?=?timing.domComplete?-?timing.domInteractive,
????loadEventTime?=?timing.loadEventEnd?-?timing.loadEventStart,
????loadTime?=?timing.loadEventEnd?-?timing.navigationStart;
????console.log('準(zhǔn)備新頁面時間耗時: '?+?readyStart);
????console.log('redirect 重定向耗時: '?+?redirectTime);
????console.log('Appcache 耗時: '?+?appcacheTime);
????console.log('unload 前文檔耗時: '?+?unloadEventTime);
????console.log('DNS 查詢耗時: '?+?lookupDomainTime);
????console.log('TCP連接耗時: '?+?connectTime);
????console.log('request請求耗時: '?+?requestTime);
????console.log('請求完畢至DOM加載: '?+?initDomTreeTime);
????console.log('解析DOM樹耗時: '?+?domReadyTime);
????console.log('load事件耗時: '?+?loadEventTime);
????console.log('加載時間耗時: '?+?loadTime);
}
通過上面的時間戳計(jì)算可以得到幾個關(guān)鍵步驟所消耗的時間,對前端有意義的幾個過程主要是解析DOM樹耗時、load事件耗時和整個加載過程耗時等,不過在頁面性能獲取時我們可以盡量獲取更詳細(xì)的數(shù)據(jù)信息,以供后面分析。除了資源加載解析的關(guān)鍵點(diǎn)計(jì)時,performance還提供了一些其他方面的功能,我們可以根據(jù)具體需要進(jìn)行選擇使用。
performance.memory?// 內(nèi)存占用的具體數(shù)據(jù)
performance.now()?// performance.now()方法返回當(dāng)前網(wǎng)頁自performance.timing到現(xiàn)在的時間,可以精確到微秒,用于更加精確的計(jì)數(shù)。但實(shí)際上,目前網(wǎng)頁性能通過毫秒來計(jì)算就足夠了。
performance.getEntries()?// 獲取頁面所有加載資源的performance timing情況。瀏覽器獲取網(wǎng)頁時,會對網(wǎng)頁中每一個對象(腳本文件、樣式表、圖片文件等)發(fā)出一個HTTP請求。performance.getEntries方法以數(shù)組形式返回所有請求的時間統(tǒng)計(jì)信息。
performance.navigation?// performance還可以提供用戶行為信息,例如網(wǎng)絡(luò)請求的類型和重定向次數(shù)等,一般都存放在performance.navigation對象里面。
performance.navigation.redirectCount?// 記錄當(dāng)前網(wǎng)頁重定向跳轉(zhuǎn)的次數(shù)。
參考資料:https://www.w3.org/TR/resource-timing/。
二、 Profile工具
Performance Timing API描述了頁面資源從加載到解析各個階段的執(zhí)行關(guān)鍵點(diǎn)時間記錄,但是無法統(tǒng)計(jì)JavaScript執(zhí)行過程中系統(tǒng)資源的占用情況。Profile是Chrome和Firefox等標(biāo)準(zhǔn)瀏覽器提供的一種用于測試頁面腳本運(yùn)行時系統(tǒng)內(nèi)存和CPU資源占用情況的API,以Chrome瀏覽器為例,結(jié)合Profile,可以實(shí)現(xiàn)以下幾個功能。
1.分析頁面腳本執(zhí)行過程中最耗資源的操作
2.記錄頁面腳本執(zhí)行過程中JavaScript對象消耗的內(nèi)存與堆棧的使用情況
3.檢測頁面腳本執(zhí)行過程中CPU占用情況
使用console.profile()和console.profileEnd()就可以分析中間一段代碼執(zhí)行時系統(tǒng)的內(nèi)存或CPU資源的消耗情況,然后配合瀏覽器的Profile查看比較消耗系統(tǒng)內(nèi)存或CPU資源的操作,這樣就可以有針對性地進(jìn)行優(yōu)化了。
console.profile();
// TODOS,需要測試的頁面邏輯動作
for?(let?i?=?0;?i?<?100000;?i++)?{
????console.log(i *?i);
}
console.profileEnd();
三、 頁面埋點(diǎn)計(jì)時
使用Profile可以在一定程度上幫助我們分析頁面的性能,但缺點(diǎn)是不夠靈活。實(shí)際項(xiàng)目中,我們不會過多關(guān)注頁面內(nèi)存或CPU資源的消耗情況,因?yàn)镴avaScript有自動內(nèi)存回收機(jī)制。我們關(guān)注更多的是頁面腳本邏輯執(zhí)行的時間。除了Performance Timing的關(guān)鍵過程耗時計(jì)算,我們還希望檢測代碼的具體解析或執(zhí)行時間,這就不能寫很多的console.profile()和console.profileEnd()來逐段實(shí)現(xiàn),為了更加簡單地處理這種情況,往往選擇通過腳本埋點(diǎn)計(jì)時的方式來統(tǒng)計(jì)每部分代碼的運(yùn)行時間。
頁面JavaScript埋點(diǎn)計(jì)時比較容易實(shí)現(xiàn),和Performance Timing記錄時間戳有點(diǎn)類似,我們可以記錄JavaScript代碼開始執(zhí)行的時間戳,后面在需要記錄的地方埋點(diǎn)記錄結(jié)束時的時間戳,最后通過差值來計(jì)算一段HTML解析或JavaScript解析執(zhí)行的時間。為了方便操作,可以將某個操作開始和結(jié)束的時間戳記錄到一個數(shù)組中,然后分析數(shù)組之間的間隔就得到每個步驟的執(zhí)行時間,下面來看一個時間點(diǎn)記錄和分析的例子。
let?timeList?=?[];
function?addTime(tag)?{
????timeList.push({
????????"tag":?tag,
????????"time": +new?Date
????});
}
addTime("loading");
timeList.push({
????"tag":?"load",
????"time": +new?Date()
});
// TODOS,load加載時的操作
timeList.push({
????"tag":?"load",
????"time": +new?Date()
});
timeList.push({
????"tag":?"process",
????"time": +new?Date()
});
// TODOS,process處理時的操作
timeList.push({
????"tag":?"process",
????"time": +new?Date()
});
parseTime(timeList);?// 輸出{load: 時間毫秒數(shù),process: 時間毫秒數(shù)}
function?parseTime(time)?{
????let?timeStep?=?{},
????endTime;
????for?(let?i?=?0,?len?=?time.length;?i?<?len;?i++)?{
????????if?(!time[i])?continue;
????????endTime?=?{};
????????for?(let?j?=?i?+?1;?j?<?len;?j++)?{
????????????if?(time[j]?&&?time[i].tag?==?time[j].tag)?{
????????????????endTime.tag?=?time[j].tag;
????????????????endTime.time?=?time[j].time;
????????????????time[j]?=?null;
????????????}
????????}
????????if?(endTime.time?>=?0?&&?endTime.tag)?{
????????????timeStep[endTime.tag]?=?endTime.time?-?time[i].time;
????????}
????}
????return?timeStep;
}
這種方式常常在移動端頁面中使用,因?yàn)橐苿佣藶g覽器HTML解析和JavaScript執(zhí)行相對較慢,通常為了進(jìn)行性能優(yōu)化,我們需要找到頁面中執(zhí)行JavaScript耗時的操作,如果將關(guān)鍵JavaScript的執(zhí)行過程進(jìn)行埋點(diǎn)計(jì)時并上報(bào),就可以輕松找出JavaScript執(zhí)行慢的地方,并有針對性地進(jìn)行優(yōu)化。
四、資源加載時序圖
我們還可以借助瀏覽器或其他工具的資源加載時序圖來幫助分析頁面資源加載過程中的性能問題。這種方法可以粗粒度地宏觀分析瀏覽器的所有資源文件請求耗時和文件加載順序情況,如保證CSS和數(shù)據(jù)請求等關(guān)鍵性資源優(yōu)先加載,JavaScript文件和頁面中非關(guān)鍵性圖片等內(nèi)容延后加載。如果因?yàn)槟硞€資源的加載十分耗時而阻塞了頁面的內(nèi)容展示,那就要著重考慮。所以,我們需要通過資源加載時序圖來輔助分析頁面上資源加載順序的問題。
圖2為使用Fiddler獲取瀏覽器訪問地址?http://www.jixianqianduan.com?時的資源加載時序圖。根據(jù)此圖,我們可以很直觀地看到頁面上各個資源加載過程所需要的時間和先后順序,有利于找出加載過程中比較耗時的文件資源,幫助我們有針對性地進(jìn)行優(yōu)化。