近来在做一个较为通用的前端机能监控平台,区分于前端非常监控,前端的机能监控重要须要上报和展现的是前端的机能数据,包括首页衬着时刻、每一个页面的白屏时刻、每一个页面一切资本的加载时刻以及每一个页面中所以要求的响应时刻等等。
本文的引见的是怎样设想一个通用的jssdk,能够以较小的侵入性,自动上报前端的机能数据。重要采纳的是Performance API以及sendBeacon要领等等。重要参考的是google analytics以及阿里云前端机能监控平台的实践。
在我的项目中运用nestjs作为后端框架,nestjs是基于express的一款圆满支撑typescript,类java spring的node后端框架。本文重要着重与怎样上报机能数据,后端处置惩罚逻辑比较简朴,不会详细引见,因而不须要相识怎样运用nestjs。本文的重要内容包括了:
- 依据Performance API猎取前端机能数据
- 什么时刻应当上报机能数据
- 怎样上报机能数据
原文在我的博客中,迎接star
https://github.com/forthealll…
一、依据Performance API 猎取前端机能数据
本文上报的前端机能数据包括两部份,一是经由过程Performance API取得的机能数据,二是自定义的在每一个页面应当上报的数据。
起首来看经由过程Performance API所猎取的数据,该数据也包括了两个部份,当前页面的机能相干数据以及当前页面资本加载和异步要求的相干数据。
(1)、Performance API 所供应的机能数据
window.performance.timing会返回一个对象,该对象包括了种种与页面衬着所相干的数据。本文不会详细去引见该对象,只给出依据该对象盘算相干机能数据的要领:
let times = {};
let t = window.performance.timing;
//重定向时刻
times.redirectTime = t.redirectEnd - t.redirectStart;
//dns查询耗时
times.dnsTime = t.domainLookupEnd - t.domainLookupStart;
//TTFB 读取页面第一个字节的时刻
times.ttfbTime = t.responseStart - t.navigationStart;
//DNS 缓存时刻
times.appcacheTime = t.domainLookupStart - t.fetchStart;
//卸载页面的时刻
times.unloadTime = t.unloadEventEnd - t.unloadEventStart;
//tcp衔接耗时
times.tcpTime = t.connectEnd - t.connectStart;
//request要求耗时
times.reqTime = t.responseEnd - t.responseStart;
//剖析dom树耗时
times.analysisTime = t.domComplete - t.domInteractive;
//白屏时刻
times.blankTime = t.domLoading - t.fetchStart;
//domReadyTime
times.domReadyTime = t.domContentLoadedEventEnd - t.fetchStart;
在上面的times对象中就包括了机能相干的属性,依据performance.timing中的相干属性盘算就能够获得效果。在这里我们认为domReadyTime就是首屏加载的时刻,另外也能够自定义的要领上报首屏的时刻:
比方有些场景能够认为是dom增量最大的点为首屏衬着完成的时刻,也有一些场景能够定义可见的dom在增量最大处为首屏衬着完成的时刻。
(2)、Performance API 所供应的资本加载和要求数据
能够经由过程window.performance.getEntries()来猎取资本的加载和要求相干的数据。每一个页面中,须要去加载许多资本比方js、css等等,同时在页面中还会存在一些异步要求。经由过程window.performance.getEntries()能够取得这些资本加载和异步要求所相干的数据。我们能够经由过程以下的体式格局来猎取加载和异步要求的数据:
let entryTimesList = [];
let entryList = window.performance.getEntries();
entryList.forEach((item,index)=>{
let templeObj = {};
let usefulType = ['navigation','script','css','fetch','xmlhttprequest','link','img'];
if(usefulType.indexOf(item.initiatorType)>-1){
templeObj.name = item.name;
templeObj.nextHopProtocol = item.nextHopProtocol;
//dns查询耗时
templeObj.dnsTime = item.domainLookupEnd - item.domainLookupStart;
//tcp链接耗时
templeObj.tcpTime = item.connectEnd - item.connectStart;
//要求时刻
templeObj.reqTime = item.responseEnd - item.responseStart;
//重定向时刻
templeObj.redirectTime = item.redirectEnd - item.redirectStart;
entryTimesList.push(templeObj);
}
});
我们经由过程window.performance.getEntries()取得一个带有资本加载和异步要求相干数据的数组,然后依据数组中每一个元素的initiatorType属性来过滤出属性为[‘navigation’,’script’,’css’,’fetch’,’xmlhttprequest’,’link’,’img’]之一的元素数据。
(3)、注重点
- 经由过程window.performance.timing所获的的页面衬着所相干的数据,在单页运用中转变了url但不革新页面的状况下是不会更新的。因而假如仅仅经由过程该api是没法取得每一个子路由所对应的页面衬着的时刻。假如须要上报切换路由状况下每一个子页面从新render的时刻,须要自定义上报。
- 经由过程window.performance.getEntries()所猎取的资本加载和异步要求所相干的数据,在页面切换路由的时刻会从新的盘算,能够完成自动的上报。
二、什么时刻上报机能数据
接着来肯定应当什么时刻上报机能数据,由于要处置惩罚pv(接见量)和uv(自力用户接见量),平常认为一次上报就是一次接见,那末什么时刻上报机能数据呢。在我的体系中挑选在一下场景下举行一次前端机能数据的上报:
- 页面加载和从新革新
- 页面切换路由
- 页面地点的tab标签从新变得可见
针对上述的3种场景,特别是切换路由的状况,假如切换路由是经由过程转变hash值来完成的,那末只须要监听hashchange事宜,假如是经由过程html5的history api来转变url的,那末须要从新定义pushstate和replacestate事宜。详细的做法能够看我的上一篇文章:在单页运用中,怎样文雅的监听url的变化。
直接给出history完成路由场景下监听url转变的计划:
var _wr = function(type) {
var orig = history[type];
return function() {
var rv = orig.apply(this, arguments);
var e = new Event(type);
e.arguments = arguments;
window.dispatchEvent(e);
return rv;
};
};
history.pushState = _wr('pushState');
history.replaceState = _wr('replaceState');
然后我们就能够依据上述场景,离别监听响应的事宜,从而完成前端机能数据的上报:
addEvent(window,'load',function(e){
...deal with something
});
//监控history基础上完成的单页路由中url的变化
addEvent(window,'replaceState', function(e) {
...deal with something
});
addEvent(window,'pushState', function(e) {
...deal with something
});
//经由过程hash切换来完成路由的场景
addEvent(window,'hashchange',function(e){
...deal with something
});
addEvent('document','visibilitychang',function(e){
...deal with something
})
addEvent是一个兼容IE和规范DOM事宜流模子的事宜。
三、怎样上报机能数据
那末怎样上报机能数据呢,我们第一回响反映就是经由过程ajax要求的情势来上报前端机能数据。这类要领有一些缺点,比方必需对跨域做特别处置惩罚以及假如页面烧毁后,响应的ajax要领并不肯定发送胜利等题目。
个中跨域的题目比较好处置惩罚,最难处理的题目是第二点:
就是假如页面烧毁,那末对应的ajax要领并不肯定能胜利发送。
我们能够依据google analytics(GA)中的要领,依据浏览器的兼容性以及url的长度,来采纳差别的要领上报机能数据,重要道理是:
经由过程动态建立img标签的体式格局,在img.src中拼接url的体式格局发送要求,不存在跨域限定。假如url太长,则才用sendBeacon的体式格局发送要求,假如sendBeacon要领不兼容,则发送ajax post同步要求
(1)、sendBeacon要领
处理在文档卸载或许页面封闭后没法完成异步ajax要求的题目,许多状况下我们会把异步变成同步。在页面卸载的unload或许beforeunload事宜中实行同步要领挪用。
然则同步要领挪用存在一个题目,就是会推延A页面切换进入B页面的时刻。而sendBeacon要领处理了该题目,简朴来讲:
sendBeacon要领在页面烧毁期,能够异步的发送数据,因而不会形成相似同步ajax要求那样的壅塞题目,也不会影响下一个页面的衬着
sendBeacon的挪用体式格局为:
navigator.sendBeacon(url [, data]);
data能够为: ArrayBufferView, Blob, DOMString, 或许 FormData
为了发送参数,我们平常data制定为Blob的情势。另外还要注重的是,在sendBeacon的要求头header中,不支撑Content-Type为“application/json; charset=utf-8”。
在sendBeacon的header中,只支撑一下3种情势的Content—Type:
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
平常制定为application/x-www-form-urlencoded,完全的经由过程sendBeacon来发送要求的例子以下:
function sendBeacon(url,data){
//推断支不支撑navigator.sendBeacon
let headers = {
type: 'application/x-www-form-urlencoded'
};
let blob = new Blob([JSON.stringify(data)], headers);
navigator.sendBeacon(url,blob);
}
后端怎样处置惩罚sendBeacon要求呢,sendBeacon在的要求头中发送的是一个相似与POST的要求,因而能够相似于处置惩罚post一样来处置惩罚sendBeacon要求。
平常我们商定ajax要求的content—type为:“application/json; charset=utf-8”,而sendBeacon要求的content-type为:“application/x-www-form-urlencoded”,如许在后端处置惩罚中,就能够区分是一般的ajax post要求照样sendBeacon要求。
另外,在处置惩罚要求的时刻假如存在跨域题目,经由过程cors跨域的体式格局来处置惩罚,后端须要设置:allow-control-allow-origin等,能够经由过程express的cors包,来简化设置:
async function bootstrap() {
const app = await NestFactory.create(ApplicationModule,instance);
app.use(cors());
await app.listen(3000)
}
bootstrap();
(2)动态建立img标签的情势
经由过程动态建立img标签的情势,指定src属性所指定的url来发送要求,起首不受跨域的限定,其次img标签动态插进去,会耽误页面的卸载保证图片的插进去,因而能够保证在页面的烧毁期,要求能够发作。
下面是一个动态建立img标签的例子:
function imgReport(url, data) {
if (!url || !data) {
return;
}
let image = document.createElement('img');
let items = [];
items = JSON.Parse(data);
let name = 'img_' + (+new Date());
image.onload = image.onerror = function () {
};
let newUrl = url + (url.indexOf('?') < 0 ? '?' : '&') + items.join('&');
image.src = newUrl;
}
另外,我们在动态建立img标签发送要求的时刻,要求的是一张图片,在后端处置惩罚的时刻,要在末端将这个图片返回,如许前端的image.onload要领才会被触发。我们以要求的地点为:localhost:8080/1.jpg为例,后端的处置惩罚逻辑为:
@Controller('1.jpg')
export class AppUploadController {
constructor(private readonly appService: AppService) {}
@Get()
getUpload(@Req() req,@Res() res): void {
...deal with some thing
res.sendFile(join(__dirname, '..', 'public/1.jpg'))
}
}
在get要求的处置惩罚中,我们经由过程res.sendFile(join(__dirname, ‘..’, ‘public/1.jpg’))将图片返回后,如许前端的image的onload要领才会被挪用。
(3)同步ajax post要求
动态建立img标签的要领,拼接url的时刻存在肯定的题目,由于浏览器对url的长度是有限定的。而sendBeacon要领兼容性不是很好,末了兜底的处置惩罚体式格局就是发送同步的ajax要求,同步的ajax要求前面说过,会在页面烧毁期之前实行,虽然会有肯定水平的壅塞下一个页面的衬着。
function xmlLoadData(url,data) {
var client = new XMLHttpRequest();
client.open("POST", url,false);
client.setRequestHeader("Content-Type", "application/json; charset=utf-8");
client.send(JSON.stringify(data));
}
(4)综合处理计划
平常起首拼接照顾参数的完全的url,推断url的长度,假如url的长度小于浏览器许可的最大长度内,那末经由过程动态建立img标签的情势来发送前端机能数据,假如url太长,则推断浏览器是不是支撑sendBeacon要领,假如支撑,则经由过程sendBeacon要领来发送要求,不然发送同步的ajax要求。
function dealWithUrl(url,appId){
let times = performanceInfo(appId);
let items = decoupling(times);
let urlLength = (url + (url.indexOf('?') < 0 ? '?' : '&') + items.join('&')).length;
if(urlLength<2083){
imgReport(url,times);
}else if(navigator.sendBeacon){
sendBeacon(url,times);
}else{
xmlLoadData(url,times);
}
}