前端首屏耗時測量方法

做者:前端時空-銳銳愛南球
公衆號「前端時空」每日一題活動
回覆「1」看面試題 | 回覆「2」看答案

在進行前端優化的時候,咱們須要關注各類性能相關的指標,瞭解這些指標表明的含義才能更好地進行性能優化。javascript

重點須要關注的指標

  1. DOMContentLoaded 指標:DOM 解析完畢的時間點,在這個時間點,通常會去獲取請求,綁定事件等
  2. Load 指標:指頁面須要的資源所有加載完畢的時間頁面初始化首屏耗時。

對於前面兩個指標,咱們能夠簡單地經過Performance API來進行統計和計算css

function getInitTime() {  if(window.performace && performance.timing) {     // 計算DOMContentedLoaded 耗時 const DOMContentLoadedTime = performance.timing.domInteractive - performance.timing.navigationStart // 計算Load耗時 const loadTime = performance.timing.loadEventStart- performance.timing.navigationStart }}複製代碼

可是對於初始化首屏耗時,這就得看這個指標怎麼定義。前端

在咱們團隊,對這個指標是頁面初始化加載須要的所有數據渲染到頁面以後,頁面對於用戶處於一種數據 ready,而且可進行交互操做的狀態時,這個時間點和 performance.timing.navigationStart 的差值。vue

舉個栗子🌰
java

在這張首屏初始化時序圖中,咱們能夠看到藍線表明的是 DOMContentLoaded 的時間點,前面的都是靜態資源 css、js,圖片等的加載,而紅線則表明 Load 時間點,在藍色和紅色之間,有未加載完的資源,也有前端發起的請求,在紅線以後,有的請求尚未返回,可是這個接口的數據是首屏渲染所必須的。node

假如咱們對某個頁面的首屏耗時進行計算,有可能由於一些接口慢而致使在 Load 以後還在加載,頁面還處於 loading 的狀態,而用戶不能看到首屏須要的所有數據。(因此纔要優化,給我所有的 mock 數據,我能秒開你信不信)ios

解決思路

既然前端須要的首屏數據來自接口,那咱們就先對每一個接口進行打點,計算接口的耗時,以 axios 爲例面試

const timeMap = {}// 請求攔截器axios.interceptors.request.use(config => { if (timeMap[config.url]) { timeMap[config.url].startTime = new Date().getTime(); } else { timeMap[config.url] = { startTime: 0, endTime: 0, total: 0 } timeMap[config.url].startTime = new Date().getTime(); } return config;}, function (error) { return Promise.reject(error);});// 響應攔截器axios.interceptors.response.use(response => { if (timeMap[response.config.url] && timeMap[response.config.url].startTime) { timeMap[response.config.url].endTime = new Date().getTime(); timeMap[response.config.url].total = timeMap[response.config.url].endTime - timeMap[response.config.url].startTime; } console.log(timeMap) return response; // 響應返回參數}, (responseError) => { // 響應錯誤 console.log("responseError", responseError);});複製代碼

這樣咱們就能夠拿到每一個具體的接口的耗時,而後初始化的時候,能夠拿最後一個接口的 endTime 來做爲首屏的耗時點。vue-cli

事情真的就這麼簡單嗎?咱們想一下,數據拿回來以後,咱們要作的就是把數據渲染到頁面上,瀏覽器渲染數據的過程也是須要消耗時間的,也就是說,實際消耗的時間 > 最慢接口的結束時間axios

假如咱們能夠拿到 DOM 變化或者 DOM 渲染的時間點,那麼是否是就解決了。這時候,MutationObverser 就派上用場了。

DOM 規範中的 MutationObserver() 構造函數——是 MutationObserver 接口內容的一部分——建立並返回一個新的觀察器,它會在觸發指定 DOM 事件時,調用指定的回調函數。MutationObserver 對 DOM 的觀察不會當即啓動;而必須先調用 observe() 方法來肯定,要監聽哪一部分的 DOM 以及要響應哪些更改。

當即擼出以下代碼:

if (window.MutationObserver) {    let changeTime = 0;  	//對DOM進行觀察 const observer = new MutationObserver((mutationList, mutationObserver) => { changeTime = new Date().getTime(); console.log(mutationList) console.log('dom Change:', (changeTime - performance.timing.navigationStart) / 1000); }); const node = document.querySelector('#app'); //觀察子節點的變化 observer.observe(node, { childList: true, subtree: true }) //必定時間以後取消對DOM的觀察 setTimeout(() => { observer.disconnect(); }, timeOut) }複製代碼

咱們能夠在控制檯看到每一次DOM發生變化時,頁面有哪些元素進行了建立,銷燬和更新

既然這樣,當數據返回的時候,也就是 DOM 的變化波動最爲激烈的時候,考慮 DOM 暴增的那個點做爲首屏耗時的時間點進行統計。也就是 mutationList 中 addNotes 數量最多的那個點進行統計。

可是,若是說初始化首屏數據依賴於兩個接口,接口 1 比較快,接口 2 比較慢,而接口 1 包含大量數據,接口 2 包含了少許數據,那麼咱們按照 DOM 暴增的點進行統計,可能就會比實際提早,因此咱們應該兩種思路結合起來。在頁面所有接口都返回以後,DOM 變化的那個點做爲首屏的點。

對上面的代碼進行整合修改

const timeMap = {}// 請求攔截器axios.interceptors.request.use(config => { if (timeMap[config.url]) { timeMap[config.url].startTime = new Date().getTime(); } else { timeMap[config.url] = { startTime: 0, endTime: 0, total: 0 } timeMap[config.url].startTime = new Date().getTime(); } return config;}, function (error) { return Promise.reject(error);});// 響應攔截器axios.interceptors.response.use(response => { if (timeMap[response.config.url] && timeMap[response.config.url].startTime) { timeMap[response.config.url].endTime = new Date().getTime(); timeMap[response.config.url].total = timeMap[response.config.url].endTime - timeMap[response.config.url].startTime; } console.log(timeMap) return response; // 響應返回參數}, (responseError) => { // 響應錯誤 console.log("responseError", responseError);});if (window.MutationObserver) { let changeTime = 0; //對DOM進行觀察 const observer = new MutationObserver((mutationList, mutationObserver) => { let isRequestAllEnd = true; const urlKeys = Object.keys(timeMap); urlKeys.forEach((d) => { if(!d.endTime) { isRequestAllEnd = false; } }) if(isRequestAllEnd) { changeTime = new Date().getTime(); console.log('init time', (changeTime - performance.timing.navigationStart) / 1000); observer.disconnect(); } }); const node = document.querySelector('#app'); //觀察子節點的變化 observer.observe(node, { childList: true, subtree: true }); }複製代碼

參考文檔

developer.mozilla.org/zhCN/docs/W…

developer.mozilla.org/zhCN/docs/W…

                                               

                                        關注公衆號,回覆「加羣」,進入學習交流羣

                                                               往期精彩文章


                                                         前端響應式你瞭解多少?
                                        理想主義團隊的開源做品之Chameleon跨端框架
                                                     手把手教你搞定 vue-cli4 配置
                                                 一分鐘理解 JavaScript 發佈訂閱模式
相關文章
相關標籤/搜索