前端性能监控:window.performance

window.performance 是W3C性能小组引入的新的API,目前IE9以上的浏览器都支持。一个performance对象的完整结构如下图所示:

Window.performance

虽然叫 Timing API 但是用起来却是 window.performance

1
2
3
4
// 兼容性写法
const performance = window.performance ||
window.msPerformance ||
window.webkitPerformance;

memory字段代表JavaScript对内存的占用。

performance.memory(内存)

  • usedJSHeapSize
    JS 对象(包括V8引擎内部对象)占用的内存
  • totalJSHeapSize
    可使用的内存
  • jsHeapSizeLimit
    内存大小限制

performance.navigation(我从哪里来)

  • redirectCount
    如果有重定向的话,页面通过几次重定向跳转而来
  • type

  • 0 即 TYPE_NAVIGATENEXT 正常进入的页面(非刷新、非重定向等)

  • 1 即 TYPE_RELOAD 通过 window.location.reload() 刷新的页面
  • 2 即 TYPE_BACK_FORWARD 通过浏览器的前进后退按钮进入的页面(历史记录)
  • 255 即 TYPE_UNDEFINED 非以上方式进入的页面

navigation字段统计的是一些网页导航相关的数据:

  1. redirectCount:重定向的数量(只读),但是这个接口有同源策略限制,即仅能检测同源的重定向;
  2. type 返回值应该是0,1,2 中的一个。分别对应三个枚举值:
  • 0 : TYPE_NAVIGATE (用户通过常规导航方式访问页面,比如点一个链接,或者一般的get方式)
  • 1 : TYPE_RELOAD (用户通过刷新,包括JS调用刷新接口等方式访问页面)
  • 2 : TYPE_BACK_FORWARD (用户通过后退按钮访问本页面)

最重要的是timing字段的统计数据,它包含了网络、解析等一系列的时间数据。

performance.timing(时间)

2.2.1 timing API

timing的整体结构如上图所示:

各字段的含义如下:

  • startTime:有些浏览器实现为navigationStart,代表浏览器开始unload前一个页面文档的开始时间节点。比如我们当前正在浏览baidu.com,在地址栏输入google.com并回车,浏览器的执行动作依次为:unload当前文档(即http://baidu.com)->请求下一文档(即http://google.com)。navigationStart的值便是触发unload当前文档的时间节点。

如果当前文档为空,则navigationStart的值等于fetchStart。

  • redirectStart和redirectEnd:如果页面是由redirect而来,则redirectStart和redirectEnd分别代表redirect开始和结束的时间节点;
  • unloadEventStart和unloadEventEnd:如果前一个文档和请求的文档是同一个域的,则unloadEventStart和unloadEventEnd分别代表浏览器unload前一个文档的开始和结束时间节点。否则两者都等于0;
  • fetchStart是指在浏览器发起任何请求之前的时间值。在fetchStart和domainLookupStart之间,浏览器会检查当前文档的缓存;
  • domainLookupStart和domainLookupEnd分别代表DNS查询的开始和结束时间节点。如果浏览器没有进行DNS查询(比如使用了cache),则两者的值都等于fetchStart;
  • connectStart和connectEnd分别代表TCP建立连接和连接成功的时间节点。如果浏览器没有进行TCP连接(比如使用持久化连接webscoket),则两者都等于domainLookupEnd;
  • secureConnectionStart:可选。如果页面使用HTTPS,它的值是安全连接握手之前的时刻。如果该属性不可用,则返回undefined。如果该属性可用,但没有使用HTTPS,则返回0;
  • requestStart代表浏览器发起请求的时间节点,请求的方式可以是请求服务器、缓存、本地资源等;
  • responseStart和responseEnd分别代表浏览器收到从服务器端(或缓存、本地资源)响应回的第一个字节和最后一个字节数据的时刻;
  • domLoading代表浏览器开始解析html文档的时间节点。我们知道IE浏览器下的document有readyState属性,domLoading的值就等于readyState改变为loading的时间节点;
  • domInteractive代表浏览器解析html文档的状态为interactive时的时间节点。domInteractive并非DOMReady,它早于DOMReady触发,代表html文档解析完毕(即dom tree创建完成)但是内嵌资源(比如外链css、js等)还未加载的时间点;
  • domContentLoadedEventStart:代表DOMContentLoaded事件触发的时间节点:

页面文档完全加载并解析完毕之后,会触发DOMContentLoaded事件,HTML文档不会等待样式文件,图片文件,子框架页面的加载(load事件可以用来检测HTML页面是否完全加载完毕(fully-loaded))。

  • domContentLoadedEventEnd:代表DOMContentLoaded事件完成的时间节点,此刻用户可以对页面进行操作,也就是jQuery中的domready时间;
  • domComplete:html文档完全解析完毕的时间节点;
  • loadEventStart和loadEventEnd分别代表onload事件触发和结束的时间节点

2.2.2 计算性能指标

可以使用Navigation.timing 统计到的时间数据来计算一些页面性能指标,比如DNS查询耗时、白屏时间、domready等等。如下:

  • DNS查询耗时 = domainLookupEnd - domainLookupStart
  • TCP链接耗时 = connectEnd - connectStart
  • request请求耗时 = responseEnd - responseStart
  • 解析dom树耗时 = domComplete - domInteractive
  • 白屏时间 = domloadng - fetchStart
  • domready时间 = domContentLoadedEventEnd - fetchStart
  • onload时间 = loadEventEnd - fetchStart

    所以根据上面的时间点,我们可以计算常规的性能值,如下:
    (使用该api时需要在页面完全加载完成之后才能使用,最简单的办法是在window.onload事件中读取各种数据,因为很多值必须在页面完全加载之后才能得出。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
var timing = window.performance && window.performance.timing;
var navigation = window.performance && window.performance.navigation;


重定向次数:
var redirectCount = navigation && navigation.redirectCount;

跳转耗时:
var redirect = timing.redirectEnd - timing.redirectStart;

APP CACHE 耗时:
var appcache = Math.max(timing.domainLookupStart - timing.fetchStart, 0);

DNS 解析耗时:
var dns = timing.domainLookupEnd - timing.domainLookupStart;

TCP 链接耗时:
var conn = timing.connectEnd - timing.connectStart;

等待服务器响应耗时(注意是否存在cache):
var request = timing.responseStart - timing.requestStart;

内容加载耗时(注意是否存在cache):
var response = timing.responseEnd - timing.responseStart;

总体网络交互耗时,即开始跳转到服务器资源下载完成:
var network = timing.responseEnd - timing.navigationStart;

渲染处理:
var processing = (timing.domComplete || timing.domLoading) - timing.domLoading;

抛出 load 事件:
var load = timing.loadEventEnd - timing.loadEventStart;

总耗时:
var total = (timing.loadEventEnd || timing.loadEventStart || timing.domComplete || timing.domLoading) - timing.navigationStart;

可交互:
var active = timing.domInteractive - timing.navigationStart;

请求响应耗时,即 T0,注意cache:
var t0 = timing.responseStart - timing.navigationStart;

首次出现内容,即 T1:
var t1 = timing.domLoading - timing.navigationStart;

内容加载完毕,即 T3:
var t3 = timing.loadEventEnd - timing.navigationStart;

2.2.3 Resource timing API

Resource timing API是用来统计静态资源相关的时间信息,详细的内容请参考W3C Resource timing。这里我们只介绍performance.getEntries方法,它可以获取页面中每个静态资源的请求,【以百度移动版首页的logo为例】如下:

比较有用的几个属性:

name:资源的链接

initiatorType: 初始类型(注意这个类型并不准确,例如在css中的图片资源会这个值显示css,所以还是推荐用name中的后缀名)

duration: 资源的总耗时(包括等待时长,请求时长,响应时长 相当于responseEnd - startTime)

transferSize: 转换后的文件大小(略大于encodedBodySize, 为什么我取这个呢,因为这个值是和chrome的devtool Network里的size一致) 

可以看到performance.getEntries返回一个数组,数组的每个元素代表对应的静态资源的信息,比如上图展示的第一个元素对应的资源类型initiatorType是图片img,请求花费的时间就是duration的值。

关于Resource timing API的使用场景,感兴趣的同学可以深入研究。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
;
(function() {

handleAddListener('load', getTiming)

function handleAddListener(type, fn) {
if(window.addEventListener) {
window.addEventListener(type, fn)
} else {
window.attachEvent('on' + type, fn)
}
}

function getTiming() {
try {
var time = performance.timing;
var timingObj = {};

var loadTime = (time.loadEventEnd - time.loadEventStart) / 1000;

if(loadTime < 0) {
setTimeout(function() {
getTiming();
}, 200);
return;
}

timingObj['重定向时间'] = (time.redirectEnd - time.redirectStart) / 1000;
timingObj['DNS解析时间'] = (time.domainLookupEnd - time.domainLookupStart) / 1000;
timingObj['TCP完成握手时间'] = (time.connectEnd - time.connectStart) / 1000;
timingObj['HTTP请求响应完成时间'] = (time.responseEnd - time.requestStart) / 1000;
timingObj['DOM开始加载前所花费时间'] = (time.responseEnd - time.navigationStart) / 1000;
timingObj['DOM加载完成时间'] = (time.domComplete - time.domLoading) / 1000;
timingObj['DOM结构解析完成时间'] = (time.domInteractive - time.domLoading) / 1000;
timingObj['脚本加载时间'] = (time.domContentLoadedEventEnd - time.domContentLoadedEventStart) / 1000;
timingObj['onload事件时间'] = (time.loadEventEnd - time.loadEventStart) / 1000;
timingObj['页面完全加载时间'] = (timingObj['重定向时间'] + timingObj['DNS解析时间'] + timingObj['TCP完成握手时间'] + timingObj['HTTP请求响应完成时间'] + timingObj['DOM结构解析完成时间'] + timingObj['DOM加载完成时间']);

for(item in timingObj) {
console.log(item + ":" + timingObj[item] + '毫秒(ms)');
}

console.log(performance.timing);

} catch(e) {
console.log(timingObj)
console.log(performance.timing);
}
}
})();

<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8" />
<title></title>
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
<link rel="stylesheet" type="text/css" href="//cdn.bootcss.com/bootstrap/4.0.0-alpha.6/css/bootstrap.css" />
<script src=""></script>
<script type="text/javascript" src=""></script>
</head>

<body>

</body>

</html>

性能监控,数据上报

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
function WBP(option) {
try {
Date.prototype.Format = function(fmt) { // author: meizz
var o = {
"M+": this.getMonth() + 1, // 月份
"d+": this.getDate(), // 日
"h+": this.getHours(), // 小时
"m+": this.getMinutes(), // 分
"s+": this.getSeconds(), // 秒
"q+": Math.floor((this.getMonth() + 3) / 3), // 季度
"S": this.getMilliseconds() // 毫秒
};
if(/(y+)/.test(fmt))
fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length));
for(var k in o)
if(new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
return fmt;
}

var w = document.documentElement.clientWidth || document.body.clientWidth; //浏览器宽度
var h = document.documentElement.clientHeight || document.body.clientHeight; //浏览器高度
var opt = {
ps: 1, //采样比率
device: {
clientW: w,
clientH: h,
},
t: new Date().Format("yyyy-MM-dd hh:mm:ss"),
pid: "123",
errorList: []
};    
for(var i in option) {      
opt[i] = option[i];    
}    
function getExplore() {
var sys = {},
ua = navigator.userAgent.toLowerCase(),
s;
(s = ua.match(/rv:([\d.]+)\) like gecko/)) ? sys.ie = s[1]:
(s = ua.match(/msie ([\d\.]+)/)) ? sys.ie = s[1] :
(s = ua.match(/edge\/([\d\.]+)/)) ? sys.edge = s[1] :
(s = ua.match(/firefox\/([\d\.]+)/)) ? sys.firefox = s[1] :
(s = ua.match(/(?:opera|opr).([\d\.]+)/)) ? sys.opera = s[1] :
(s = ua.match(/micromessenger\/([\d\.]+)/i)) ? sys.weixin = s[1] :
(s = ua.match(/chrome\/([\d\.]+)/)) ? sys.chrome = s[1] :
(s = ua.match(/version\/([\d\.]+).*safari/)) ? sys.safari = s[1] :
(s = ua.match(/weibo\/([\d\.]+)/i)) ? sys.weibo = s[1] : 0;
// 根据关系进行判断
if(sys.ie) return('IE: ' + sys.ie)
if(sys.edge) return('EDGE: ' + sys.edge)
if(sys.firefox) return('Firefox: ' + sys.firefox)
if(sys.weixin) return('weixin: ' + sys.weixin)
if(sys.chrome) return('Chrome: ' + sys.chrome)
if(sys.opera) return('Opera: ' + sys.opera)
if(sys.safari) return('Safari: ' + sys.safari)
if(sys.weibo) return('weibo: ' + sys.weibo)
return 'Unkonwn'
}

function getOS() {
var userAgent = 'navigator' in window && 'userAgent' in navigator && navigator.userAgent.toLowerCase() || '';
var vendor = 'navigator' in window && 'vendor' in navigator && navigator.vendor.toLowerCase() || '';
var appVersion = 'navigator' in window && 'appVersion' in navigator && navigator.appVersion.toLowerCase() || '';

if(/mac/i.test(appVersion)) return 'MacOSX'
if(/win/i.test(appVersion)) return 'windows'
if(/linux/i.test(appVersion)) return 'linux'
if(/iphone/i.test(userAgent) || /ipad/i.test(userAgent) || /ipod/i.test(userAgent)) 'ios'
if(/android/i.test(userAgent)) return 'android'
if(/win/i.test(appVersion) && /phone/i.test(userAgent)) return 'windowsPhone'
}

function perforPage() {
var performance = window.performance ||
window.msPerformance ||
window.webkitPerformance;
if(!performance) return;
var time = performance.timing
var timingObj = {};

timingObj['rdit'] = (time.redirectEnd - time.redirectStart) / 1000; //页面重定向时间
timingObj['dnst'] = (time.domainLookupEnd - time.domainLookupStart) / 1000; // DNS解析时间
timingObj['tcpt'] = (time.connectEnd - time.connectStart) / 1000; //TCP建立时间
timingObj['httpt'] = (time.responseEnd - time.requestStart) / 1000; //HTTP请求响应完成时间
timingObj['domst'] = (time.responseEnd - time.navigationStart) / 1000; //DOM开始加载前所花费时间
timingObj['doml'] = (time.domComplete - time.domLoading) / 1000; //DOM加载完成时间
timingObj['domr'] = (time.domInteractive - time.domLoading) / 1000; //DOM结构解析完成时间
timingObj['jst'] = (time.domContentLoadedEventEnd - time.domContentLoadedEventStart) / 1000; //脚本加载时间
timingObj['loadt'] = (time.loadEventEnd - time.loadEventStart) / 1000; //onload事件时间
timingObj['allt'] = (timingObj['rdit'] + timingObj['dnst'] + timingObj['tcpt'] + timingObj['httpt'] + timingObj['domr'] + timingObj['doml']);

timingObj['reqt'] = time.responseEnd - time.requestStart || 0; //request请求耗时
timingObj['wit'] = time.responseStart - time.navigationStart || 0; // 白屏时间


return timingObj;
}

window.addEventListener('error', function(e) {
var defaults = {
msg: '',
h: ''
};
defaults.msg = e.type + ":" + e.target.localName + ' is load error';
defaults.h = e.target.href || e.target.currentSrc || e.target.src;

if(e.target != window) opt.errorList.push(defaults)
}, true);

window.onerror = function(msg, _url, line, col, error) {
var defaults = {
line: line,
col: col || (window.event && window.event.errorCharacter) || 0,
msg: error && error.stack ? error.stack.toString() : msg
};
opt.errorList.push(defaults)
};

window.addEventListener('unhandledrejection', function(e) {

var defaults = {
msg: e.type + ":" + e.reason
};

console.log(defaults)
opt.errorList.push(defaults)
});

function formatParams(data, random) {
var arr = [];
for(var name in data) {
arr.push(encodeURIComponent(name) + "=" + encodeURIComponent(data[name]));
}
if(random) {
arr.push(("v=" + Math.random()).replace(".", ""));
}
return arr.join("&");
}

function _confuse(str, sign) {
if(typeof str !== 'string' || !str) {
return '';
}
var newStr = '';
for(var i = 0; i < str.length; i++) {
newStr += String.fromCharCode(str.charCodeAt(i) + sign * 2);
}
return newStr;
};

var softdog = {
encrypt: function(str) {
return _confuse(str, 1);
},
decrypt: function(str) {
return _confuse(str, -1);
}
};

setTimeout(function() {
opt.device.pfm = perforPage();
opt.device.browser = getExplore();
opt.device.os = getOS()
opt.referrer=document.referrer && document.referrer !== location.href ? document.referrer : '', //页面来源
var str = softdog.encrypt(JSON.stringify(opt))
console.log(str)
console.log(softdog.decrypt(str))

if(Math.random()<=opt.ps){ //抽样采样
var img = new Image();
img.src = './img/sky.png?opt=' + str;
}
}, 200);

} catch(e) {}
}

数据上报字符串加密解密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/***
* @module {Object} softdog 简单的字符串加密|解密,防止密码提交拦截
* @interface Softdog
* @method encrypt ---------- 字符串加密
* @method decrypt ---------- 字符串解密
*/
(function (factory) {
if (typeof module === "object" && typeof module.exports === "object") {
var v = factory(require, exports);
if (v !== undefined) module.exports = v;
}
else if (typeof define === "function" && define.amd) {
define(["require", "exports"], factory);
}
})(function (require, exports) {
"use strict";
// 功能函数
function _confuse(str, sign) {
if (typeof str !== 'string' || !str) {
return '';
}
var newStr = '';
for (var i = 0; i < str.length; i++) {
newStr += String.fromCharCode(str.charCodeAt(i) + sign * 2);
}
return newStr;
}
;
var softdog = {
encrypt: function (str) {
return _confuse(str, 1);
},
decrypt: function (str) {
return _confuse(str, -1);
}
};
return softdog;
});

仿百度上报

1
2
3
4
5
6
7
8
9
10
11
12
13
14
router.afterEach( ( to, from, next ) => {
setTimeout(()=>{
var _hmt = _hmt || [];
(function() {
//每次执行前,先移除上次插入的代码
document.getElementById('baidu_tj') && document.getElementById('baidu_tj').remove();
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?xxxx";
hm.id = "baidu_tj"
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();
},0);
} );

js 判断 wifi and ‘2g’, ‘3g’, ‘4g’

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
var connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection || { tyep: 'unknown' };
var type_text = ['unknown', 'ethernet', 'wifi', '2g', '3g', '4g', 'none'];

alert(connection.type);
var re_el = document.getElementById("re");
var btn_el = document.getElementById("btn");
function get_status() {
if (typeof (connection.type) == "number") {
connection.type_text = type_text[connection.type];
} else {
connection.type_text = connection.type;
}
if (typeof (connection.bandwidth) == "number") {
if (connection.bandwidth > 10) {
connection.type = 'wifi';
} else if (connection.bandwidth > 2) {
connection.type = '3g';
} else if (connection.bandwidth > 0) {
connection.type = '2g';
} else if (connection.bandwidth == 0) {
connection.type = 'none';
} else {
connection.type = 'unknown';
}
}
var html = 'Type : ' + connection.type_text;
html += 'Bandwidth : ' + connection.bandwidth;
html += 'isOnline : ' + navigator.onLine;
re_el.innerHTML = html;
}

btn_el.onclick = function () {
re_el.innerHTML = 'Waiting...';
get_status();
}

设备选型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function getInfo(){
var s = "";
s += " 网页可见区域宽:"+ document.body.clientWidth+"\n";
s += " 网页可见区域高:"+ document.body.clientHeight+"\n";
s += " 网页可见区域宽:"+ document.body.offsetWidth + " (包括边线和滚动条的宽)"+"\n";
s += " 网页可见区域高:"+ document.body.offsetHeight + " (包括边线的宽)"+"\n";
s += " 网页正文全文宽:"+ document.body.scrollWidth+"\n";
s += " 网页正文全文高:"+ document.body.scrollHeight+"\n";
s += " 网页被卷去的高(ff):"+ document.body.scrollTop+"\n";
s += " 网页被卷去的高(ie):"+ document.documentElement.scrollTop+"\n";
s += " 网页被卷去的左:"+ document.body.scrollLeft+"\n";
s += " 网页正文部分上:"+ window.screenTop+"\n";
s += " 网页正文部分左:"+ window.screenLeft+"\n";
s += " 屏幕分辨率的高:"+ window.screen.height+"\n";
s += " 屏幕分辨率的宽:"+ window.screen.width+"\n";
s += " 屏幕可用工作区高度:"+ window.screen.availHeight+"\n";
s += " 屏幕可用工作区宽度:"+ window.screen.availWidth+"\n";
s += " 你的屏幕设置是 "+ window.screen.colorDepth +" 位彩色"+"\n";
s += " 你的屏幕设置 "+ window.screen.deviceXDPI +" 像素/英寸"+"\n";
alert (s);
}
getInfo();