前言
上一篇主要說了腳本錯誤捕獲,資源加載錯誤捕獲,和promise捕獲。本篇記錄下接口異常捕獲、白屏監測、加載時間、性能指標,卡頓指標,pv。
接口異常捕獲
原理
重寫xmlhttprequest的open和send方法,使其在上報前進行標記和計時,監聽其load、error、about事件,當發生相應的事件進行上報。
注意!open事件中進行標記xhr需要排除上報url,否則會發生無限循環。
axios同理,可以進行自行封裝。排除上報地址後,對應的計時監測和上報。
export default function injectXHR ( ) {
let xhr = window. XMLHttpRequest;
let open = xhr. prototype. open;
xhr. prototype. open = function ( method, url, async , user, password) {
if ( ! url. match ( /logstores/ ) && ! url. match ( /sockjs/ ) ) {
this . logData = { method, url, async , user, password } ;
}
return open. apply ( this , arguments) ;
} ;
let send = xhr. prototype. send;
xhr. prototype. send = function ( body) {
if ( this . logData) {
let startTime = Date. now ( ) ;
let handler = ( type) => ( event) => {
let duration = Date. now ( ) - startTime;
let status = this . status;
let statusText = this . statusText;
tracker. send ( {
kind: "stability" ,
type: "xhr" ,
eventType: type,
pathname: this . logData. url,
status: status + "-" + statusText,
duration,
response: this . response ? JSON . stringify ( this . response) : "" ,
params: body || "" ,
} ) ;
} ;
this . addEventListener ( "load" , handler ( "load" ) , false ) ;
this . addEventListener ( "error" , handler ( "error" ) , false ) ;
this . addEventListener ( "abort" , handler ( "abort" ) , false ) ;
}
return send. apply ( this , arguments) ;
} ;
}
白屏監測
原理
這個白屏需要和首屏渲染那些指標區分出來,這個是頁面異常了白屏的反饋。
通過document.elementsFromPoint,進行取點,這個取點一般情況是根據頁面設計搞得,爲了方便也可以橫着豎着取個十字。然後可以獲取到裏面的元素,因爲這個能獲取最裏面的元素,所以就通過這個進行判斷,如果排除html,body等容器標籤,仍有標籤存在,那就不是空白點,否則是空白點,根據業務需要設定空白點比值,大於這個值就是白屏。
export default function blankscreen ( ) {
let wrapperElements = [ "html" , "body" , "#container" , ".content" ] ;
let emptyPoints = 0 ;
function getSelector ( element) {
if ( element. id) {
return "#" + id;
} else if ( element. className) {
return (
"." +
element. className
. split ( " " )
. filter ( ( i) => ! ! i)
. join ( "." )
) ;
} else {
return element. nodeName. toLowerCase ( ) ;
}
}
function iswrapper ( element) {
let selector = getSelector ( element) ;
if ( wrapperElements. indexOf ( selector) != - 1 ) {
emptyPoints++ ;
}
}
onload ( function ( ) {
for ( let i = 1 ; i <= 9 ; i++ ) {
let xele = document. elementsFromPoint (
( window. innerWidth * i) / 10 ,
window. innerHeight / 2
) ;
let yele = document. elementsFromPoint (
window. innerWidth / 2 ,
( window. innerHeight * i) / 10
) ;
iswrapper ( xele[ 0 ] ) ;
iswrapper ( yele[ 0 ] ) ;
}
if ( emptyPoints >= 10 ) {
let centerElement = document. elementsFromPoint (
window. innerWidth / 2 ,
window. innerHeight / 2
) ;
tracker. send ( {
kind: "stability" ,
type: "blank" ,
emptyPoints,
screen: window. screen. width + "X" + window. screen. height,
viewPoint: window. innerWidth + "X" + window. innerHeight,
selector: getSelector ( centerElement[ 0 ] ) ,
} ) ;
}
} ) ;
}
加載時間
原理
主要利用瀏覽器的api performance.timing製作上報即可。
字段
含義
navigationStart
初始化頁面,在同一個瀏覽器上下文中前一個頁面unload的時間戳,如果沒有前一個頁面的unload,則與fetchStart值相等
redirectStart
第一個HTTP重定向發生的時間,有跳轉且是同域的重定向,否則爲0
redirectEnd
最後一個重定向完成時的時間,否則爲0
fetchStart
瀏覽器準備好使用http請求獲取文檔的時間,這發生在檢查緩存之前
domainLookupStart
DNS域名開始查詢的時間,如果有本地的緩存或keep-alive則時間爲0
domainLookupEnd
DNS域名結束查詢的時間
connectStart
TCP開始建立連接的時間,如果是持久連接,則與fetchStart值相等
secureConnectionStart
https 連接開始的時間,如果不是安全連接則爲0
connectEnd
TCP完成握手的時間,如果是持久連接則與fetchStart值相等
requestStart
HTTP請求讀取真實文檔開始的時間,包括從本地緩存讀取
requestEnd
HTTP請求讀取真實文檔結束的時間,包括從本地緩存讀取
responseStart
返回瀏覽器從服務器收到(或從本地緩存讀取)第一個字節時的Unix毫秒時間戳
responseEnd
返回瀏覽器從服務器收到(或從本地緩存讀取,或從本地資源讀取)最後一個字節時的Unix毫秒時間戳
unloadEventStart
前一個頁面的unload的時間戳 如果沒有則爲0
unloadEventEnd
與unloadEventStart相對應,返回的是unload函數執行完成的時間戳
domLoading
返回當前網頁DOM結構開始解析時的時間戳,此時document.readyState變成loading,並將拋出readyStateChange事件
domInteractive
返回當前網頁DOM結構結束解析、開始加載內嵌資源時時間戳,document.readyState 變成interactive,並將拋出readyStateChange事件(注意只是DOM樹解析完成,這時候並沒有開始加載網頁內的資源)
domContentLoadedEventStart
網頁domContentLoaded事件發生的時間
domContentLoadedEventEnd
網頁domContentLoaded事件腳本執行完畢的時間,domReady的時間
domComplete
DOM樹解析完成,且資源也準備就緒的時間,document.readyState變成complete.並將拋出readystatechange事件
loadEventStart
load 事件發送給文檔,也即load回調函數開始執行的時間
loadEventEnd
load回調函數執行完成的時間
階段名
描述
計算方式
意義
unload
前一個頁面卸載耗時
unloadEventEnd – unloadEventStart-redirect
重定向耗時redirectEnd– redirectStart 重定向的時間
appCache
緩存耗時
domainLookupStart – fetchStart
讀取緩存的時間
dns
DNS 解析耗時
domainLookupEnd – domainLookupStart
可觀察域名解析服務是否正常
tcp
TCP
連接耗時
connectEnd – connectStart 建立連接的耗時
ssl
SSL 安全連接耗時
connectEnd – secureConnectionStart
反映數據安全連接建立耗時
ttfb
Time to First Byte(TTFB)網絡請求耗時
responseStart – requestStart
TTFB是發出頁面請求到接收到應答數據第一個字節所花費的毫秒數
response
響應數據傳輸耗時
responseEnd – responseStart
觀察網絡是否正常
dom
DOM解析耗時
domInteractive – responseEnd
觀察DOM結構是否合理,是否有JS阻塞頁面解析
dcl
DOMContentLoaded 事件耗時
domContentLoadedEventEnd – domContentLoadedEventStart
當 HTML 文檔被完全加載和解析完成之後,DOMContentLoaded 事件被觸發,無需等待樣式表、圖像和子框架的完成加載
resources
資源加載耗時
domComplete – domContentLoadedEventEnd
可觀察文檔流是否過大
domReady
DOM階段渲染耗時
domContentLoadedEventEnd – fetchStart
DOM樹和頁面資源加載完成時間,會觸發domContentLoaded事件
首次渲染耗時
首次渲染耗時
responseEnd-fetchStart
加載文檔到看到第一幀非空圖像的時間,也叫白屏時間
首次可交互時間
首次可交互時間
domInteractive-fetchStart
DOM樹解析完成時間,此時document.readyState爲interactive
首包時間耗時
首包時間
responseStart-domainLookupStart
DNS解析到響應返回給瀏覽器第一個字節的時間
頁面完全加載時間
頁面完全加載時間
loadEventStart - fetchStart -onLoad
onLoad
事件耗時
loadEventEnd – loadEventStart
export default function timing ( ) {
onload ( function ( ) {
setTimeout ( ( ) => {
const {
fetchStart,
connectStart,
connectEnd,
requestStart,
responseStart,
responseEnd,
domLoading,
domInteractive,
domContentLoadedEventStart,
domContentLoadedEventEnd,
loadEventStart,
} = performance. timing;
tracker. send ( {
kind: "experience" ,
type: "timing" ,
connectTime: connectEnd - connectStart,
ttfbTime: responseStart - requestStart,
responseTime: responseEnd - responseStart,
parseDomTime: loadEventStart - domLoading,
domContentLoadedTime:
domContentLoadedEventEnd - domContentLoadedEventStart,
timeToInteractive: domInteractive - fetchStart,
loadTime: loadEventStart - fetchStart,
} ) ;
} , 3000 ) ;
} ) ;
}
性能指標
原理
主要利用瀏覽器api performanceObserver。
< html>
< head>
< script>
var observer = new PerformanceObserver ( list => {
list. getEntries ( ) . forEach ( entry => {
if ( console) {
console. log ( "Name: " + entry. name +
", Type: " + entry. entryType +
", Start: " + entry. startTime +
", Duration: " + entry. duration + "\n" ) ;
}
} )
} ) ;
observer. observe ( { entryTypes: [ 'resource' , 'mark' , 'measure' ] } ) ;
performance. mark ( 'registered-observer' ) ;
function clicked ( elem) {
performance. measure ( 'button clicked' ) ;
}
</ script>
</ head>
< body>
< button onclick = " clicked(this)" > Measure</ button>
</ body>
</ html>
這個api可以很方便的對某些特定元素進行計算時間。比如你的某個dom是異步加載上來的,那麼只要在這個dom上添加屬性elementtiming,值隨意,那麼就能計算這個元素渲染到頁面的FMP。
< img... elementtiming= 'foobar' / >
< p elementtiming= 'yehuozhili' > This is text I care about. < / p>
...
< script>
const observer = new PerformanceObserver ( ( list) => {
let perfEntries = list. getEntries ( ) ;
} ) ;
observer. observe ( { type: 'element' , buffered: true } ) ;
< / script>
下面這個表的字段可以在chrome裏看見,點擊錄製之後刷新網站在性能裏。
字段
描述
備註
FP
First Paint(首次繪製)
包括了任何用戶自定義的背景繪製,它是首先將像素繪製到屏幕的時刻
FCP
First Content Paint(首次內容繪製)
是瀏覽器將第一個 DOM 渲染到屏幕的時間,可能是文本、圖像、SVG等,這其實就是白屏時間
FMP
First Meaningful Paint(首次有意義繪製)
頁面有意義的內容渲染的時間
LCP
(Largest Contentful Paint)(最大內容渲染)
代表在viewport中最大的頁面元素加載的時間
DCL
(DomContentLoaded)(DOM加載完成)
當 HTML 文檔被完全加載和解析完成之後,DOMContentLoaded 事件被觸發,無需等待樣式表、圖像和子框架的完成加載
L
(onLoad)
當依賴的資源全部加載完畢之後纔會觸發
TTI
(Time to Interactive)
可交互時間 用於標記應用已進行視覺渲染並能可靠響應用戶輸入的時間點
FID
First Input Delay(首次輸入延遲)
用戶首次和頁面交互(單擊鏈接,點擊按鈕等)到頁面響應交互的時間
這裏面第一次交互時間就是first-input,它並不是input輸入框,而是用戶的第一次交互,比如第一次點擊頁面之類的那一下就會產生。
if ( window. PerformanceObserver) {
let FMP , LCP ;
let p1 = new Promise ( ( res) => {
new PerformanceObserver ( ( entryList, observer) => {
let perf = entryList. getEntries ( ) ;
FMP = perf[ 0 ] ;
observer. disconnect ( ) ;
res ( FMP ) ;
} ) . observe ( { entryTypes: [ "element" ] } ) ;
} ) ;
let p2 = new Promise ( ( res) => {
new PerformanceObserver ( ( entryList, observer) => {
let perf = entryList. getEntries ( ) ;
LCP = perf[ 0 ] ;
observer. disconnect ( ) ;
res ( LCP ) ;
} ) . observe ( { entryTypes: [ "largest-contentful-paint" ] } ) ;
} ) ;
new Promise ( ( res) => {
new PerformanceObserver ( ( entryList, observer) => {
let lastevnet = getLastEvent ( ) ;
let perf = entryList. getEntries ( ) [ 0 ] ;
if ( perf) {
let inputDelay = perf. processingStart - perf. startTime;
let duration = perf. duration;
if ( inputDelay > 0 || duration > 0 ) {
tracker. send ( {
kind: "experience" ,
type: "fistInputDelay" ,
inputDelay,
duration,
startTime: perf. startTime,
selector: lastevnet
? getSelector ( lastevnet. path || lastevnet. target)
: "" ,
} ) ;
}
}
observer. disconnect ( ) ;
res ( perf) ;
} ) . observe ( { type: "first-input" , buffered: true } ) ;
} ) ;
Promise. all ( [ p1, p2] ) . then ( ( ) => {
let FP = performance. getEntriesByName ( "first-paint" ) [ 0 ] ;
let FCP = performance. getEntriesByName ( "first-contentful-paint" ) [ 0 ] ;
tracker. send ( {
kind: "experience" ,
type: "paint" ,
firstPaint: FP . startTime,
firstContentFulPaint: FCP . startTime,
firstMeaningFulPaint: FMP . startTime,
largestContentFulPaint: LCP . startTime,
} ) ;
} ) ;
}
卡頓指標
原理
跟上面差不多,但是需要observe longtask。50ms以上的事件會被longtask給捕捉上,這樣得到這個事件花了多少時間。
new PerformanceObserver ( ( list) => {
list. getEntries ( ) . forEach ( ( entry) => {
if ( entry. duration > 100 ) {
let lastEvent = getLastEvent ( ) ;
requestIdleCallback ( ( ) => {
tracker. send ( {
kind: "experience" ,
type: "longTask" ,
eventType: lastEvent. type,
startTime: entry. startTime,
duration: entry. duration,
selector: lastEvent
? getSelector ( lastEvent. path || lastEvent. target)
: "" ,
} ) ;
} ) ;
}
} ) ;
} ) . observe ( { entryTypes: [ "longtask" ] } ) ;
PV
原理
這個ip方面可以通過sohu腳本獲取,其他方面有個navigator.connection,可以獲取到網絡環境,往返時間之類。
用戶停留時間就是需要監聽unload事件,然後減去開始時間即可。
卸載頁面會有幾個問題,就是卸載頁面不能異步發送請求,否則發送不到,所以需要同步發送請求。
爲了減少同步發送請求所造成的性能問題使得用戶下一次頁面跳轉過於墨跡,有個api叫Navigator.sendBeacon可以解決這個問題。這api就等於同步發送,只是用戶體驗更好。
export default function pv ( ) {
var connection = navigator. connection;
tracker. send ( {
kind: "business" ,
type: "pv" ,
effectiveType: connection. effectiveType,
rtt: connection. rtt,
screen: ` ${ window. screen. width} x ${ window. screen. height} ` ,
ip: window. userip,
} ) ;
let startTime = Date. now ( ) ;
window. addEventListener (
"unload" ,
( ) => {
let stayTime = Date. now ( ) - startTime;
tracker. send ( {
kind: "business" ,
type: "stayTime" ,
stayTime,
ip: window. userip,
} ) ;
} ,
false
) ;
}
完整demo代碼