####### 背景
性能優化是所有前端人的追求,在這條路上,方法多種多樣。這篇文章,說一下利用瀏覽器的一些API,可以怎樣進行自定義性能指標及上報。
PS:后面會出一篇利用chrome開發者工具進行性能分析的哦,敬請期待~
####### 自定義性能指標介紹
自定義性能指標這里,主要要介紹的是 Performance 接口,這個接口可以獲取到當前頁面中與性能相關的信息。主要包含了Performance Timeline API、Navigation Timing API、 User Timing API 和 Resource Timing API。
Performance類型的對象可以通過調用只讀屬性 window.performance 來獲得,截止目前,其支持度已經很高了,支持性如下:

####### performance.now()
在chrome瀏覽器中返回的時間是以毫秒為單位的,更精確。
performance.now() 與 Date.now() 不同的是,返回了以微秒(百萬分之一秒)為單位的時間,更加精准。
並且與 Date.now() 會受系統程序執行阻塞的影響不同,performance.now() 的時間是以恆定速率遞增的,不受系統時間的影響(系統時間可被人為或軟件調整)。
這里主要是一些需要入侵業務代碼打點的時候,可以使用這個 API 來獲取時間戳
注意:Date.now() 輸出的是距離 1970 的毫秒數,而 performance.now() 輸出的是相對於 performance.timing.navigationStart(頁面初始化) 的時間。
使用 Date.now() 的差值並非絕對精確,因為計算時間時受系統限制(可能阻塞)。但使用 performance.now() 的差值,並不影響我們計算程序執行的精確時間。
####### window.performance.navigation
window.performance.navigation 對象提供了在指定的時間段里發生的操作相關信息,包括頁面是加載還是刷新、發生了多少次重定向等。我們可以看看:

其中,type 的取值及含義如下表:

具體數據示例:

這個數據,主要是幫助我們看看頁面重定向次數是否過多(能否減少重定向),頁面訪問的方式主要是怎樣的,針對訪問方式較多的場景我們能否做些優化。
這種情況下,我們只需要把數據直接上報,然后自己查看數據的時候,再跟具體含義結合起來理解即可。
####### window.performance.timing
window.performance.timing里面有很多的性能相關的時間戳記錄,我們來看一些常用的:

更多查看:https://developer.mozilla.org/en-US/docs/Web/API/PerformanceTiming
####### 關鍵指標
這樣,我們就可以定出一些關鍵步驟耗時:

這個數據,上報到數據平台系統。就可以看到頁面的性能情況如何,然后進行對應的優化了。不過,實際情況中,前端更關注的性能指標在首屏,比如:
- HTML 加載完成時間
- 首屏圖片加載完成時間
- 首屏接口完成加載完成時間
代碼實現
這個時候,我們可以自己手動加上一些時間點(這里的手動添加的點都推薦使用performance.now來實現),結合一起上報。代碼示例如下:
//window.loadHtmlTime 在html中的</body>標簽前面用打個時間戳即可
HTMLComplete = window.loadHtmlTime - window.performance.timing.navigationStart
//window.lastImgLoadTime 在首屏中的每張圖onload之后都更新一次這個時間戳
firstScreenImgFinished = window.lastImgLoadTime - window.performance.timing.navigationStart
//Report.SPEED.MAINCGI 在首屏中的每個接口調用成功后更新時間戳
firstScreenApiFinished = Report.SPEED.MAINCGI - window.performance.timing.navigationStart
//在所有接口打時間點
apiFinishes = Report.SPEED.LASTCGI - window.performance.timing.navigationStart);
注意:
我們在做性能埋點的時候,最好不要入侵業務代碼。這里我的想法是,每個api調用的方法,我們都返回一個Promise,這樣,我們再另外封裝一個sdk去找到這些方法,然后分別注冊then方法來計時即可。
window.performance.getEntries()
window.performance.getEntries 是一個方法,方法調用后可以獲取一個包含了頁面中所有的 HTTP 請求的時間數據的數組.這個數組是一個按startTime排序的對象數組,數組成員除了會自動根據所請求資源的變化而改變以外,還可以用mark(),measure()方法自定義添加。
其與 performance.timing 對比的差別就是沒有與 DOM 相關的屬性。而要注意的是, HTTP 請求有可能命中本地緩存,這種情況下請求響應的間隔將非常短,數據可能不准確。
我們來看看它都包含來了哪些時間,如下一個例子圖:

由圖可以看出,每個對象的屬性中除了包含資源加載過程各個階段的時間外,還有以下五個屬性:
- name:資源名稱,是資源的絕對路徑或調用mark方法自定義的名稱
- startTime:開始時間
- duration:加載時間
- entryType:資源類型,entryType類型不同數組中的對象結構也不同
* initiatorType:發起的請求者
其中,常用entryType的值含義如下:

其中,常見initiatorType的值含義如下:

關鍵指標
由以上,我們可以得出一些,我們比較關心的性能指標如下:
- 首屏圖片完成時間
- 各資源耗時(主要統計css/js資源耗時)
- FP(首次繪制時間)
- FCP(首次內容渲染時間)
代碼實現
當我們調用這個方法的時候,我們得到的是調用方法前的所有資源的數據,一些資源可能有延時,或者在一些特殊的邏輯下才加載,這種情況下,我們就需要輪詢上報了。
但是,瀏覽器考慮到這些復雜的情況,它為了我們提供了一個 PerformanceObserver,用於監測性能度量事件,在瀏覽器的性能時間軸記錄下一個新的 performance entries 的時候將會被通知.
簡單來說,我們可以利用 PerformanceObserver 做到當有性能數據產生時,主動通知你(觀察者模式),所以我們監聽自己需要的資源類型,當有這個資源的時候進行上報即可。如下代碼:
function perf_observer(list, observer) {
// Process the "measure" and "resource" event
list
.getEntries()
.map(({ name, entryType, startTime, duration }) => {
const perfObj = {
"Duration": duration,
"Entry Type": entryType,
"Name": name,
"Start Time": startTime,
};
returnJSON.stringify(obj, null, 2);
})
.forEach(console.log); // 可以加上報邏輯
}
var observer2 = new PerformanceObserver(perf_observer);
observer2.observe({entryTypes: ["paint","resource"]});
這里,關於首屏圖片的獲取和統計邏輯,我這里貼一個我使用的通用的邏輯,大家可以參考一下:
// 可以在
const getFirstScreenImageLoadTime = () => {
// 獲取所有的 img dom 節點
const images = document.getElementsByTagName('img');
const imageEntries = performance.getEntries().filter(function (entry) {
return entry.initiatorType === 'img'
});
// 獲取在首屏內的 img dom 節點
const firstScreenEntry = [];
for (let i = 0; i < images.length; i++) {
const image = images[i];
const ret = image.getBoundingClientRect();
if (ret.top < (window.innerHeight - 2) && ret.right > 0 && ret.left < (window.innerWidth - 2)) {
// 如果在首屏內
const imageEntry = imageEntries.filter(function (entry) {
return entry.name === image.src;
})[0];
imageEntry && firstScreenEntry.push(imageEntry);
}
}
// 獲取最晚加載完成的一張
let maxEntry;
if (firstScreenEntry.length >= 1) {
maxEntry = firstScreenEntry.reduce(function (prev, curr) {
if (curr.responseEnd > prev.responseEnd) {
return curr;
} else {
return prev
}
});
}
return maxEntry && maxEntry.responseEnd || null;
}
注意:
該方法有個草案,可以直接傳入過濾參數,然后得到想要的結果,如window.performance.getEntries(PerformanceEntryFilterOptions),但是我試了一下,目前 chrome 都還不支持,可以參考這里:https://developer.mozilla.org/zh-CN/docs/Web/API/Performance/getEntries。
總結
最后總結一下,我們自定義的前端比較關心的性能指標大概有:
- 白屏時間
- HTML 加載完成時間
- 首屏圖片加載完成時間
- 首屏接口完成加載完成時間
- 各資源耗時(主要統計css/js資源耗時)
- FP(首次繪制時間)
- FCP(首次內容渲染時間)
- onload時間
而這些指標的統計方法,本文都詳細寫了。可以參考一下實現方案~
