用(上)戶(帝)說,這個頁面怎麼這麼慢,還有沒有人管了?!前端
簡單而言,有三點緣由:git
一次性能重構,在千兆網速和萬元設備的條件下,頁面加載時間的提高可能只有 0.1%,可是這樣的數(土)據(豪)不具有表明性。網絡環境、硬件設備千差萬別,對於中低端設備而言,性能提高的主觀體驗更爲明顯,對應的數據變化更具有表明性。github
很多項目都會把資源上傳到 CDN。而 CDN 部分節點出現問題的時候,通常不能精準的告知「某某,你的 xx 資源掛了」,所以須要咱們主動監控。web
根據谷歌數據顯示,當頁面加載超過 10s 時,用戶會感到絕望,一般會離開當前頁面,而且極可能再也不回來。json
關於前端性能指標,W3C 定義了強大的 Performance
API,其中又包括了 High Resolution Time
、 Frame Timing
、 Navigation Timing
、 Performance Timeline
、Resource Timing
、 User Timing
等諸多具體標準。跨域
本文主要涉及 Navigation Timing
以及 Resource Timing
。截至到 2018 年中旬,各大主流瀏覽器均已完成了基礎實現。數組
Performance
API 功能衆多,其中一項,就是將頁面自身以及頁面中各個資源的性能表現(時間細節)記錄了下來。而咱們要作的就是查詢和使用。瀏覽器
讀者能夠直接在瀏覽器控制檯中輸入
performance
,查看相關 API。緩存
接下來,咱們將使用瀏覽器提供的 window.performance
對象(Performance
API 的具體實現),來實現一個簡易的前端性能監控工具。網絡
將工具命名爲 pMonitor
,含義是 performance monitor
。
const pMonitor = {}
複製代碼
既然是「5 分鐘實現一個 xxx」系列,那麼就要有取捨。所以,本文只挑選了最爲重要的兩個指標進行監控:
看了看時間,已通過去了 4 分鐘,小編表示情緒穩定,沒有一絲波動。
![]()
有關頁面加載的性能指標,能夠在 Navigation Timing
中找到。Navigation Timing
包括了從請求頁面起,到頁面完成加載爲止,各個環節的時間明細。
能夠經過如下方式獲取 Navigation Timing
的具體內容:
const navTimes = performance.getEntriesByType('navigation')
複製代碼
getEntriesByType
是咱們獲取性能數據的一種方式。performance
還提供了getEntries
以及getEntriesByName
等其餘方式,因爲「時間限制」,具體區別不在此贅述,各位看官能夠移步到此:www.w3.org/TR/performa…。
返回結果是一個數組,其中的元素結構以下所示:
{
"connectEnd": 64.15495765894057,
"connectStart": 64.15495765894057,
"domainLookupEnd": 64.15495765894057,
"domainLookupStart": 64.15495765894057,
"domComplete": 2002.5385066728431,
"domContentLoadedEventEnd": 2001.7384263440083,
"domContentLoadedEventStart": 2001.2386167400286,
"domInteractive": 1988.638474368076,
"domLoading": 271.75174283737226,
"duration": 2002.9385468372606,
"entryType": "navigation",
"fetchStart": 64.15495765894057,
"loadEventEnd": 2002.9385468372606,
"loadEventStart": 2002.7383663540235,
"name": "document",
"navigationStart": 0,
"redirectCount": 0,
"redirectEnd": 0,
"redirectStart": 0,
"requestStart": 65.28225608537441,
"responseEnd": 1988.283025689508,
"responseStart": 271.75174283737226,
"startTime": 0,
"type": "navigate",
"unloadEventEnd": 0,
"unloadEventStart": 0,
"workerStart": 0.9636893776343863
}
複製代碼
關於各個字段的時間含義,Navigation Timing Level 2 給出了詳細說明:
不難看出,細節滿滿。所以,可以計算的內容十分豐富,例如 DNS 查詢時間,TLS 握手時間等等。能夠說,只有想不到,沒有作不到~
既然咱們關注的是頁面加載,那天然要讀取 domComplete
:
const [{ domComplete }] = performance.getEntriesByType('navigation')
複製代碼
定義個方法,獲取 domComplete
:
pMonitor.getLoadTime = () => {
const [{ domComplete }] = performance.getEntriesByType('navigation')
return domComplete
}
複製代碼
到此,咱們得到了準確的頁面加載時間。
既然頁面有對應的 Navigation Timing
,那靜態資源是否是也有對應的 Timing
呢?
答案是確定的,其名爲 Resource Timing
。它包含了頁面中各個資源從發送請求起,到完成加載爲止,各個環節的時間細節,和 Navigation Timing
十分相似。
獲取資源加載時間的關鍵字爲 'resource'
, 具體方式以下:
performance.getEntriesByType('resource')
複製代碼
不難聯想,返回結果一般是一個很長的數組,由於包含了頁面上全部資源的加載信息。
每條信息的具體結構爲:
{
"connectEnd": 462.95008929525244,
"connectStart": 462.95008929525244,
"domainLookupEnd": 462.95008929525244,
"domainLookupStart": 462.95008929525244,
"duration": 0.9620853673520173,
"entryType": "resource",
"fetchStart": 462.95008929525244,
"initiatorType": "img",
"name": "https://cn.bing.com/sa/simg/SharedSpriteDesktopRewards_022118.png",
"nextHopProtocol": "",
"redirectEnd": 0,
"redirectStart": 0,
"requestStart": 463.91217466260445,
"responseEnd": 463.91217466260445,
"responseStart": 463.91217466260445,
"startTime": 462.95008929525244,
"workerStart": 0
}
複製代碼
以上爲 2018 年 7 月 7 日,在 cn.bing.com 下搜索
test
時,performance.getEntriesByType("resource")
返回的第二條結果。
咱們關注的是資源加載的耗時狀況,能夠經過以下形式得到:
const [{ startTime, responseEnd }] = performance.getEntriesByType('resource')
const loadTime = responseEnd - startTime
複製代碼
同 Navigation Timing
類似,關於 startTime
、 fetchStart
、connectStart
和 requestStart
的區別, Resource Timing Level 2 給出了詳細說明:
並不是全部的資源加載時間都須要關注,重點仍是加載過慢的部分。
出於簡化考慮,定義 10s 爲超時界限,那麼獲取超時資源的方法以下:
const SEC = 1000
const TIMEOUT = 10 * SEC
const setTime = (limit = TIMEOUT) => time => time >= limit
const isTimeout = setTime()
const getLoadTime = ({ startTime, responseEnd }) => responseEnd - startTime
const getName = ({ name }) => name
const resourceTimes = performance.getEntriesByType('resource')
const getTimeoutRes = resourceTimes
.filter(item => isTimeout(getLoadTime(item)))
.map(getName)
複製代碼
這樣一來,咱們獲取了全部超時的資源列表。
簡單封裝一下:
const SEC = 1000
const TIMEOUT = 10 * SEC
const setTime = (limit = TIMEOUT) => time => time >= limit
const getLoadTime = ({ requestStart, responseEnd }) =>
responseEnd - requestStart
const getName = ({ name }) => name
pMonitor.getTimeoutRes = (limit = TIMEOUT) => {
const isTimeout = setTime(limit)
const resourceTimes = performance.getEntriesByType('resource')
return resourceTimes.filter(item => isTimeout(getLoadTime(item))).map(getName)
}
複製代碼
獲取數據以後,須要向服務端上報:
// 生成表單數據
const convert2FormData = (data = {}) =>
Object.entries(data).reduce((last, [key, value]) => {
if (Array.isArray(value)) {
return value.reduce((lastResult, item) => {
lastResult.append(`${key}[]`, item)
return lastResult
}, last)
}
last.append(key, value)
return last
}, new FormData())
// 拼接 GET 時的url
const makeItStr = (data = {}) =>
Object.entries(data)
.map(([k, v]) => `${k}=${v}`)
.join('&')
// 上報數據
pMonitor.log = (url, data = {}, type = 'POST') => {
const method = type.toLowerCase()
const urlToUse = method === 'get' ? `${url}?${makeItStr(data)}` : url
const body = method === 'get' ? {} : { body: convert2FormData(data) }
const option = {
method,
...body
}
fetch(urlToUse, option).catch(e => console.log(e))
}
複製代碼
數據上傳的 url、超時時間等細節,因項目而異,因此須要提供一個初始化的方法:
// 緩存配置
let config = {}
/** * @param {object} option * @param {string} option.url 頁面加載數據的上報地址 * @param {string} option.timeoutUrl 頁面資源超時的上報地址 * @param {string=} [option.method='POST'] 請求方式 * @param {number=} [option.timeout=10000] */
pMonitor.init = option => {
const { url, timeoutUrl, method = 'POST', timeout = 10000 } = option
config = {
url,
timeoutUrl,
method,
timeout
}
// 綁定事件 用於觸發上報數據
pMonitor.bindEvent()
}
複製代碼
性能監控只是輔助功能,不該阻塞頁面加載,所以只有當頁面完成加載後,咱們才進行數據獲取和上報(實際上,頁面加載完成前也獲取不到必要信息):
// 封裝一個上報兩項核心數據的方法
pMonitor.logPackage = () => {
const { url, timeoutUrl, method } = config
const domComplete = pMonitor.getLoadTime()
const timeoutRes = pMonitor.getTimeoutRes(config.timeout)
// 上報頁面加載時間
pMonitor.log(url, { domeComplete }, method)
if (timeoutRes.length) {
pMonitor.log(
timeoutUrl,
{
timeoutRes
},
method
)
}
}
// 事件綁定
pMonitor.bindEvent = () => {
const oldOnload = window.onload
window.onload = e => {
if (oldOnload && typeof oldOnload === 'function') {
oldOnload(e)
}
// 儘可能不影響頁面主線程
if (window.requestIdleCallback) {
window.requestIdleCallback(pMonitor.logPackage)
} else {
setTimeout(pMonitor.logPackage)
}
}
}
複製代碼
到此爲止,一個完整的前端性能監控工具就完成了~所有代碼以下:
const base = {
log() {},
logPackage() {},
getLoadTime() {},
getTimeoutRes() {},
bindEvent() {},
init() {}
}
const pm = (function() {
// 向前兼容
if (!window.performance) return base
const pMonitor = { ...base }
let config = {}
const SEC = 1000
const TIMEOUT = 10 * SEC
const setTime = (limit = TIMEOUT) => time => time >= limit
const getLoadTime = ({ startTime, responseEnd }) => responseEnd - startTime
const getName = ({ name }) => name
// 生成表單數據
const convert2FormData = (data = {}) =>
Object.entries(data).reduce((last, [key, value]) => {
if (Array.isArray(value)) {
return value.reduce((lastResult, item) => {
lastResult.append(`${key}[]`, item)
return lastResult
}, last)
}
last.append(key, value)
return last
}, new FormData())
// 拼接 GET 時的url
const makeItStr = (data = {}) =>
Object.entries(data)
.map(([k, v]) => `${k}=${v}`)
.join('&')
pMonitor.getLoadTime = () => {
const [{ domComplete }] = performance.getEntriesByType('navigation')
return domComplete
}
pMonitor.getTimeoutRes = (limit = TIMEOUT) => {
const isTimeout = setTime(limit)
const resourceTimes = performance.getEntriesByType('resource')
return resourceTimes
.filter(item => isTimeout(getLoadTime(item)))
.map(getName)
}
// 上報數據
pMonitor.log = (url, data = {}, type = 'POST') => {
const method = type.toLowerCase()
const urlToUse = method === 'get' ? `${url}?${makeItStr(data)}` : url
const body = method === 'get' ? {} : { body: convert2FormData(data) }
const init = {
method,
...body
}
fetch(urlToUse, init).catch(e => console.log(e))
}
// 封裝一個上報兩項核心數據的方法
pMonitor.logPackage = () => {
const { url, timeoutUrl, method } = config
const domComplete = pMonitor.getLoadTime()
const timeoutRes = pMonitor.getTimeoutRes(config.timeout)
// 上報頁面加載時間
pMonitor.log(url, { domeComplete }, method)
if (timeoutRes.length) {
pMonitor.log(
timeoutUrl,
{
timeoutRes
},
method
)
}
}
// 事件綁定
pMonitor.bindEvent = () => {
const oldOnload = window.onload
window.onload = e => {
if (oldOnload && typeof oldOnload === 'function') {
oldOnload(e)
}
// 儘可能不影響頁面主線程
if (window.requestIdleCallback) {
window.requestIdleCallback(pMonitor.logPackage)
} else {
setTimeout(pMonitor.logPackage)
}
}
}
/** * @param {object} option * @param {string} option.url 頁面加載數據的上報地址 * @param {string} option.timeoutUrl 頁面資源超時的上報地址 * @param {string=} [option.method='POST'] 請求方式 * @param {number=} [option.timeout=10000] */
pMonitor.init = option => {
const { url, timeoutUrl, method = 'POST', timeout = 10000 } = option
config = {
url,
timeoutUrl,
method,
timeout
}
// 綁定事件 用於觸發上報數據
pMonitor.bindEvent()
}
return pMonitor
})()
export default pm
複製代碼
如何?是否是不復雜?甚至有點簡單~
再次看了看時間,5 分鐘什麼的,仍是不要在乎這些細節了吧 orz
![]()
若是想追(吹)求(毛)極(求)致(疵)的話,在頁面加載時,監測工具不該該佔用主線程的 JavaScript 解析時間。所以,最好在頁面觸發 onload
事件後,採用異步加載的方式:
// 在項目的入口文件的底部
const log = async () => {
const pMonitor = await import('/path/to/pMonitor.js')
pMonitor.init({ url: 'xxx', timeoutUrl: 'xxxx' })
pMonitor.logPackage()
// 能夠進一步將 bindEvent 方法從源碼中刪除
}
const oldOnload = window.onload
window.onload = e => {
if (oldOnload && typeof oldOnload === 'string') {
oldOnload(e)
}
// 儘可能不影響頁面主線程
if (window.requestIdleCallback) {
window.requestIdleCallback(log)
} else {
setTimeout(log)
}
}
複製代碼
工具在數據上報時,沒有考慮跨域問題,也沒有處理 GET
和 POST
同時存在的狀況。
5 分鐘還要什麼自行車!
若有需求,能夠自行覆蓋 pMonitor.logPackage
方法,改成動態建立 <form/>
和 <iframe/>
,或者使用更爲常見的圖片打點方式~
這個仍是須要服務端配合的嘛[認真臉.jpg]。
既能夠是每一個項目對應不一樣的上報 url,也能夠是統一的一套 url,項目分配惟一 id 做爲區分。
當超時次數在規定時間內超過約定的閾值時,郵件/短信通知開發人員。
如今僅僅針對超時資源進行了簡單統計,可是沒有上報具體的超時緣由(DNS?TCP?request? response?),這就留給讀者去優化了,動手試試吧~
本文介紹了關於頁面加載方面的性能監控, 此外,JavaScript 代碼的解析 + 執行,也是制約頁面首屏渲染快慢的重要因素(特別是單頁面應用)。下一話,小編將帶領你們 進一步探索 Performance Timeline Level 2
, 實現更多對於 JavaScript 運行時的性能監控,敬請期待~
《奇舞週刊》是360公司專業前端團隊「奇舞團」運營的前端技術社區。關注公衆號後,直接發送連接到後臺便可給咱們投稿。