Appearance
Performance
在前端性能监控方面,主要有以下几个指标:
- 白屏时间 : 从打开网站到有内容渲染出来的时间节点。
- 首屏时间 : 首屏内容渲染完毕的时间节点。
- 用户可操作时间节点 : domready 触发节点
- 总下载时间 : window.onload 的触发节点
分析
白屏时间
白屏时间指的是从用户进入网站(输入 url、刷新、跳转等方式)的时刻开始计算,一直到页面内容展示出来的时间节点 这个过程主要包括 : dns 查询、 建立 tcp 连接、发送首个 http 请求(如果使用 HTTPS 还要介入 TLS 的验证时间)、返回 html 文档、文档解析完毕。
首屏时间
window.performance 统计
参数 | 含义 | 默认值 | 备注 |
---|---|---|---|
navigationStart | 前一个网页卸载的时间 | fetchStart | .. |
unloadEventStart | 前一个网页的 unload 事件开始 | 0 | ... |
unloadEventEnd | 前一个网页的 unload 事件结束 | 0 | ... |
redirectStart | 重定向开始时间 | 0 | ... |
redirectEnd | 重定向结束时间 | 0 | ... |
--- | --- | --- | |
fetchStart | 开始请求网页 | 0 | ... |
domainLookupStart | DNS 查询开始 | 0 | ... |
domainLookupEnd | DNS 查询结束 | 0 | ... |
connectStart | 向服务器建立握手开始时间 | 0 | ... |
connectEnd | 向服务器建立握手结束 | 0 | ... |
secureConnectionStart | 安全握手开始时间 | 0 | ... |
--- | --- | --- | |
requestStart | 浏览器发起请求的时间节点(请求服务器、缓存、本地资源) | 0 | ... |
responseStart | 浏览器接收到从服务器端响应的第一个子节的时间节点 | 0 | ... |
responseEnd | 浏览器接收到从服务器端响应的最后一个子节的时间节点 | 0 | ... |
--- | --- | --- | |
domLoading | 浏览器发起请求的时间节点(请求服务器、缓存、本地资源) | 0 | ... |
domInteractive | 浏览器发起请求的时间节点(请求服务器、缓存、本地资源) | 0 | ... |
DOMContentLoadedEventStart | DOMContentLoaded 事件触发时间节点 | 0 | ... |
DOMContentLoadedEventEnd | DOMContentLoaded 事件结束时间节点 | 0 | ... |
domComplete | html 文档完全解析完毕的时间节点 | 0 | ... |
loadEventStart | document.onload 事件触发时间节点 | ||
loadEventEnd | document.onload 事件结束时间节点 |
DNS 查询耗时 = domainLookupEnd - domainLookupStart TCP 链接耗时 = connectEnd - connectStart request 请求耗时 = responseEnd - responseStart 解析 dom 树耗时 = domComplete - domInteractive 白屏时间 = domloadng - fetchStart domready 时间 = domContentLoadedEventEnd - fetchStart onload 时间 = loadEventEnd - fetchStart
js
var timing = performance.timing;
var times = {};
// 页面加载完成时间
times.loadPageTiming = timing.loadEventEnd - timing.navigationStart;
// 解析DOM树结构的时间
// [原因] 分析你的DOM树是否嵌套太多了
// 问题: 为什么 使用domComplete 而不是使用 domContentLoadedEventStart
times.domReady = timing.domContentLoadedEventStart - timing.responseEnd;
times.domReadyDomComplate = timing.domComplete - timing.responseEnd;
console.log(`domComplete - domContentLoadedEventStart = ${timing.domComplete - timing.domContentLoadedEventStart}`);
// 重定向时间
times.redirect = timing.redirectEnd - timing.redirectStart;
// DNS查询时间
// [ 原因:] DNS预加载做了没有?页面中是否存在很多不同的域名 导致域名查询时间太长
times.lookupDomain = timing.domainLookupEnd - timing.domainLookupStart;
// [重要] 读取页面第一个子节的时间, 即用户从输入URL 到获取到页面资源的时间
times.ttfp = timing.responseEnd - timing.navigationStart;
// 内容加载的时间, 即页面子节大小
// 注意: 这不是后台返回数据的时间 那个时间应该是 后台发出数据包 到资源加载完成时间,其从后台发出资源到客户端接收到第一个子节时间不能获取,
// 所以request 的时间是小于后台反应时间的,而 timing.responseEnd - timing.requestStart 也大于此时间
times.request = timing.responseEnd - timing.responseStart;
// 执行 load回调函数的时间 --- 即开发者在 document.addEventListener('load',() => { ..... }) 里面代码的执行时间
// 原因 : 太多不必要的操作都在 load回调中执行,考虑过延迟加载,按需加载策略
times.loadEventExeuctime = timing.loadEventEnd - timing.loadEventStart;
// DNS缓存时间
times.appcache = timing.domainLookupEnd - timing.fetchStart;
分析
- DNS 寻址时间:t.domainLookupEnd - t.domainLookupStart。
优化方法:检查页面是否添加了 DNS 预解析代码。
html
<link rel="dns-prefetch" href="//haitao.nosdn1.127.net" />
是否合理利用域名发散
与域名收敛
的策略。
TCP 连接耗时:t.connectEnd - t.connectStart。
首包时间: t.responseStart - t.navigationStart。
优化方法:是否加 cdn,数据可否静态化等。
- request 请求耗时:t.responseEnd - t.requestStart。
优化方法:返回内容是否已经压缩过,静态资源是否打包好等。
- 白屏时间。
白屏时间是最影响用户体验的,时间越久,用户等待就越久。
- 解析 DOM 树结构的时间:t.domComplete - t.domLoading。
优化方法:检查 dom 节点是否过多,dom 是否嵌套过深。
- 页面加载完成的时间:t.loadEventEnd - t.fetchStart。
优化方法:考虑延迟加载,懒加载,部分加载,减少首屏渲染时间。
首屏时间-打点
- js 后面的点 - js 前面的点 -= js 的加载时间
html
<script type="text/javascript">
console.log(`jsStartTime = ${+new Date()}`);
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.12.4/js/bootstrap-select.min.js"></script>
<script type="text/javascript">
console.log(`jsEndTime = ${+new Date()}`);
</script>
JsEndTime – JsStartTime = js 文件的加载时间,对吗?
不对!明显地,这个等式忽略了 js 的执行时间。js 执行代码是需要花费时间的,特别是做一些复杂的计算或频繁的 dom 操作,这个执行时间有时会达到几百毫秒。
那么,JsEndTime – JsStartTime = js 文件的加载执行时间?
依然不对!因为 CSS 文件的加载执行带来了干扰。
为什么? 因为 浏览器对资源文件的加载 是并行的,所以我们要知道 加载是并行的,执行是串行的。
所以当前面没有 js,css 的文件,那么在 JsEndTime – JsStartTime = js 文件的加载时间 + 执行时间
而如果 前面存在一个执行很长时间的资源文件, 那么在他们并行加载的时候,执行到 starttime 的时候可能此资源已经加载完成只是没有执行,那么这时候 JsEndTime – JsStartTime = js 文件的执行时间
- 资源文件中 加载另外的资源文件,那么这另外的资源文件的加载执行会阻塞后面资源文件的执行么(即阻塞后面文件的执行)
不会
所以在首屏优化的时候可以
无关紧要”的 js 不要放在负责渲染的 js 前面,这里的“无关紧要”是指和首屏渲染无关
可以看到,动态加载的 js 的执行是不会受到 html 后面外联的 js 的阻塞的影响,即是说,它的执行和后面 js 的执行顺序是不确定的。因此我们要小心处理好文件的依赖关系。当然还可以采用最不容易出错的方法:负责动态加载 js 的文件是 html 里面外联的最后一个文件
- 如果 html 的返回头包含 chunk,则它是边返回边解析的,不然就是一次性返回再解析。这个是在服务器配置的
如果包含这个返回头,那 html 文件是边返回边解析的,此时上面的计算方法是合理的。如果不包含这个头,则 html 文件是整一个返回来后才开始解析,此时上面的计算方法就少算了 html 的加载时间,也就不够精准。
如果我们想说明直出的优化程度,最好先瞧瞧你的 html 返回头。如果不包含 chunk 返回头,考虑拿 HTML5 performance 里面的 navigationStart 作为打点 1(这个 API 也是 Android4.4 及以上才支持),要不就要评估文件大小变化做点修正了;
对于没有启用 chunk 的 html,建议不要 inline 太多跟渲染首屏内容无关的 js 在里面,这样会影响渲染时间
- 写在 html 里面的 script 节点的加载和解析会影响 domContentLoaded 事件的触发时间
我们有时会用 domContentLoaded 事件代替 onload 事件,在页面准备好的时候做一些处理。然而要知道,domContentLoaded 里面的 dom 不止包含我们常说的普通 dom 节点,还包括 script 节点。
js 文件的加载时间会影响这个事件的触发事件。那 js 代码的解析时间会不会影响?所以 js 文件加载执行会影响 domContentLoaded 事件的执行时机。
- 如果我们打算在 domContentLoaded、onLoad 事件里面做一些特殊处理且这些处理比较重要(如跟渲染有关),那我们最好就不要在 html 里面直接外联一些跟渲染无关的 js 文件,可以考虑改用动态加载
方法
- performance.mark()
资源文件标记点
- performance.measure()
在浏览器性能 entries 中缓存一个 两个 mark 标记的时间测量数据
js
performance.mark('teststart');
for (var i = 0; i < 1000; i++) {
for (var j = 0; j < 1000; j++) {
var a = i + j;
}
}
performance.mark('testend');
performance.measure('test', 'teststart', 'testend');
console.dir(performance.getEntries());
可见这两个方法主要用于时间点标记和记录两个时间点间隔。
重点
entryType = 'mark' | 'measure' ,所以我们可以通过 performance.getEntriesByType('mark' | 'measure' ) 获取两个种类的测量数据,当然我们也可以通过 performance.getEntriesByName('teststart' | 'test' ) 去按照 name 去获取。
measure 的 duration 表示 markend - markstart 的时间差
performance.clearMarks()
performance.clearMeasures()
获取性能资源
performance.getEntries()
performance.getEntriesByName()
performance.getEntriesByType()
js
var resourceArr = [];
performance.getEntriesByType('resource').forEach(resource => {
// 这边记录的是资源文件的加载和处理时间
resourceArr.push({
// DNS解析时间 此资源是否使用了 DNS预解析
appcache: resource.domainLookupEnd - resource.domainLookupStart,
// 资源加载时间,即资源的大小加载时间
request: resource.responseEnd - resource.responseStart,
// 从加载到完成的整体用时
loadTiming: resource.responseEnd - resource.fetchStart,
// 资源名称
name: resource.name,
// 资源的类型 link script 还是 img
resoureceType: resource.initiatorType,
//
entryType: resource.entryType,
// 资源的大小
size: resource.encodedBodySize
});
});
console.table(resourceArr);
牵扯的事件
- onload 事件
触发的时机是 页面上所有的 DOM、样式表、脚本、图片、flash 等资源全部加载完成
- DOMContentLoaded 事件
触发的时机是:DOM 加载完成,不包含样式表、脚本、图片、flash 等资源文件
所以我们一般 会在 DOMContentLoaded 事件触发的时候去 给元素绑定事件,因为我们根本不需要去等待图片等资源文件加载完成去绑定执行。
如果是 webkit 引擎则轮询 document 的 readyState 属性,当值为 loaded 或者 complete 时则触发 DOMContentLoaded 事件,对 webkit525 之后版本直接可以注册 DOMContentLoaded 事件
- document.readyState