前言
上一篇主要说了脚本错误捕获,资源加载错误捕获,和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代码