前端性能监控实践(一)

1,302 阅读10分钟

前端页面性能是一个非常核心的用户体验指标。

window.performance

Performance是一个前端性能监控的API,用来检测页面的性能,是W3C性能小组引入进来的一个新的API。

一般用来检测到白屏时间、首屏时间、用户可操作的时间节点,页面总下载的时间、DNS查询的时间、TCP链接的时间等。

它的结构如下

performance = {
    // memory 是非标准属性,只在 Chrome 有
    memory: {
        usedJSHeapSize:  16100000, // JS 对象(包括V8引擎内部对象)占用的内存,一定小于 totalJSHeapSize
        totalJSHeapSize: 35100000, // 可使用的内存
        jsHeapSizeLimit: 793000000 // 内存大小限制
    },
    
    navigation: {
        redirectCount: 0, // 如果有重定向的话,页面通过几次重定向跳转而来
        type: 0           // 0   即 TYPE_NAVIGATENEXT 正常进入的页面(非刷新、非重定向等)
                          // 1   即 TYPE_RELOAD       通过 window.location.reload() 刷新的页面
                          // 2   即 TYPE_BACK_FORWARD 通过浏览器的前进后退按钮进入的页面(历史记录)
                          // 255 即 TYPE_UNDEFINED    非以上方式进入的页面
    },
    
    timing: {
        // 在同一个浏览器上下文中,前一个网页(与当前页面不一定同域)unload 的时间戳,如果无前一个网页 unload ,则与 fetchStart 值相等
        navigationStart: 1441112691935,

        // 前一个网页(与当前页面同域)unload 的时间戳,如果无前一个网页 unload 或者前一个网页与当前页面不同域,则值为 0
        unloadEventStart: 0,

        // 和 unloadEventStart 相对应,返回前一个网页 unload 事件绑定的回调函数执行完毕的时间戳
        unloadEventEnd: 0,

        // 第一个 HTTP 重定向发生时的时间。有跳转且是同域名内的重定向才算,否则值为 0 
        redirectStart: 0,

        // 最后一个 HTTP 重定向完成时的时间。有跳转且是同域名内部的重定向才算,否则值为 0 
        redirectEnd: 0,

        // 浏览器准备好使用 HTTP 请求抓取文档的时间,这发生在检查本地缓存之前
        fetchStart: 1441112692155,

        // DNS 域名查询开始的时间,如果使用了本地缓存(即无 DNS 查询)或持久连接,则与 fetchStart 值相等
        domainLookupStart: 1441112692155,

        // DNS 域名查询完成的时间,如果使用了本地缓存(即无 DNS 查询)或持久连接,则与 fetchStart 值相等
        domainLookupEnd: 1441112692155,

        // HTTP(TCP) 开始建立连接的时间,如果是持久连接,则与 fetchStart 值相等
        // 注意如果在传输层发生了错误且重新建立连接,则这里显示的是新建立的连接开始的时间
        connectStart: 1441112692155,

        // HTTP(TCP) 完成建立连接的时间(完成握手),如果是持久连接,则与 fetchStart 值相等
        // 注意如果在传输层发生了错误且重新建立连接,则这里显示的是新建立的连接完成的时间
        // 注意这里握手结束,包括安全连接建立完成、SOCKS 授权通过
        connectEnd: 1441112692155,

        // HTTPS 连接开始的时间,如果不是安全连接,则值为 0
        secureConnectionStart: 0,

        // HTTP 请求读取真实文档开始的时间(完成建立连接),包括从本地读取缓存
        // 连接错误重连时,这里显示的也是新建立连接的时间
        requestStart: 1441112692158,

        // HTTP 开始接收响应的时间(获取到第一个字节),包括从本地读取缓存
        responseStart: 1441112692686,

        // HTTP 响应全部接收完成的时间(获取到最后一个字节),包括从本地读取缓存
        responseEnd: 1441112692687,

        // 开始解析渲染 DOM 树的时间,此时 Document.readyState 变为 loading,并将抛出 readystatechange 相关事件
        domLoading: 1441112692690,

        // 完成解析 DOM 树的时间,Document.readyState 变为 interactive,并将抛出 readystatechange 相关事件
        // 注意只是 DOM 树解析完成,这时候并没有开始加载网页内的资源
        domInteractive: 1441112693093,

        // DOM 解析完成后,网页内资源加载开始的时间
        // 在 DOMContentLoaded 事件抛出前发生
        domContentLoadedEventStart: 1441112693093,

        // DOM 解析完成后,网页内资源加载完成的时间(如 JS 脚本加载执行完毕)
        domContentLoadedEventEnd: 1441112693101,

        // DOM 树解析完成,且资源也准备就绪的时间,Document.readyState 变为 complete,并将抛出 readystatechange 相关事件
        domComplete: 1441112693214,

        // load 事件发送给文档,也即 load 回调函数开始执行的时间
        // 注意如果没有绑定 load 事件,值为 0
        loadEventStart: 1441112693214,

        // load 事件的回调函数执行完毕的时间
        loadEventEnd: 1441112693215
    }
};

performance的处理模型

可以看见的是整个流程

  • 上一个文档卸载
  • 重定向
  • 浏览器准备好使用http抓取文档
  • 检查本地缓存
  • 查询DNS域名
  • TCP建立连接
  • HTTP请求、响应
  • 渲染DOM树并解析
  • 网页开始加载资源
  • 准备就绪触发load事件执行回调函数

性能指标

在前端标准中,有很多指标来定义页面性能

  • 页面加载时长
    指DOM load事件被触发完成。
    作为一个原生api它具有接受度高、感知明显的优点;但是同样使用它无法准确反映页面加载的性能,容易受到特殊情况的影响。
    后来W3C引入了首次渲染 / 首次内容渲染,首次渲染是指第⼀个非网页背景像素渲染,⾸次内容渲染是指第一个⽂本、图像、背景图片或非白色 canvas/SVG 渲染。
    在目前大多数网站里面这两个指标是没有明显差异的,因为目前网站架构倾向于单页应用,它的html结构是非常小的。

  • 确定统计起始点 ( navigationStart vs fetchStart )
    页面性能统计的起始点时间,应该是用户输入网址回车后开始等待的时间。一个是通过navigationStart获取,相当于在 URL 输入栏回车或者页面按 F5 刷新的时间点;另外一个是通过 fetchStart,相当于浏览器准备好使用 HTTP 请求获取文档的时间。
    从开发者实际分析使用的场景,浏览器重定向、卸载页面的耗时对页面加载分析并无太大作用;通常建议使用 fetchStart 作为统计起始点。

  • 首字节
    主文档返回第一个字节的时间,是页面加载性能比较重要的指标。对用户来说一般无感知,对于开发者来说,则代表访问网络后端的整体响应耗时。

  • 白屏时间
    用户看到页面展示出现一个元素的时间。很多人认为白屏时间是页面返回的首字节时间,但这样其实并不精确,因为头部资源还没加载完毕,页面也是白屏。
    相对来说具备「白屏时间」统计意义的指标,可以取 domLoading - fetchStart,此时页面开始解析 DOM 树,页面渲染的第一个元素也会很快出现。
    从 W3C Navigation Timing Level 2 的方案设计,可以直接采用 domInteractive - fetchStart ,此时页面资源加载完成,即将进入渲染环节。

  • 首屏时间
    首屏时间是指页面第一屏所有资源完整展示的时间。这是一个对用户来说非常直接的体验指标,但是对于前端却是一个非常难以统计衡量的指标。
    具备一定意义上的指标可以使用,domContentLoadedEventEnd - fetchStart,甚至使用loadEventStart - fetchStart,此时页面 DOM 树已经解析完成并且显示内容。

以下给出统计页面性能指标的方法。

  var performance = window.performance;

  if (!performance) {
    // 当前浏览器不支持
    console.log('你的浏览器不支持 performance 接口');
    return;
  }

  var t = performance.timing;
  var times = {};

  //【重要】页面加载完成的时间
  //【原因】这几乎代表了用户等待页面可用的时间
  times.loadPage = t.loadEventEnd - t.navigationStart;

  //【重要】解析 DOM 树结构的时间
  //【原因】反省下你的 DOM 树嵌套是不是太多了!
  times.domReady = t.domComplete - t.responseEnd;

  //【重要】重定向的时间
  //【原因】拒绝重定向!比如,http://example.com/ 就不该写成 http://example.com
  times.redirect = t.redirectEnd - t.redirectStart;

  //【重要】DNS 查询时间
  //【原因】DNS 预加载做了么?页面内是不是使用了太多不同的域名导致域名查询的时间太长?
  // 可使用 HTML5 Prefetch 预查询 DNS ,见:[HTML5 prefetch](http://segmentfault.com/a/1190000000633364)           
  times.lookupDomain = t.domainLookupEnd - t.domainLookupStart;

  //【重要】读取页面第一个字节的时间
  //【原因】这可以理解为用户拿到你的资源占用的时间,加异地机房了么,加CDN 处理了么?加带宽了么?加 CPU 运算速度了么?
  // TTFB 即 Time To First Byte 的意思
  // 维基百科:https://en.wikipedia.org/wiki/Time_To_First_Byte
  times.ttfb = t.responseStart - t.navigationStart;

  //【重要】内容加载完成的时间
  //【原因】页面内容经过 gzip 压缩了么,静态资源 css/js 等压缩了么?
  times.request = t.responseEnd - t.requestStart;

  //【重要】执行 onload 回调函数的时间
  //【原因】是否太多不必要的操作都放到 onload 回调函数里执行了,考虑过延迟加载、按需加载的策略么?
  times.loadEvent = t.loadEventEnd - t.loadEventStart;

  // DNS 缓存时间
  times.appcache = t.domainLookupStart - t.fetchStart;

  // 卸载页面的时间
  times.unloadEvent = t.unloadEventEnd - t.unloadEventStart;

  // TCP 建立连接完成握手的时间
  times.connect = t.connectEnd - t.connectStart;

window.onerror 错误捕获事件

顾名思义,onerror事件一般用来捕获页面错误,作为性能监控其中不可缺少的一块,此举可以帮助我们手机页面错误信息,即时定位错误并修改。

当JavaScript运行错误(包括语法错误)发生时,window会触发一个ErrorEvent接口的error事件,并执行window.onerror()。

当一项资源(如<img>或<script>)加载失败,加载资源的元素会触发一个Event接口的error事件,并执行onerror()处理函数。这些error事件不会向上冒泡到window。

加载一个全局的error事件处理函数可用于自动收集错误报告。

语法

window.onerror = function (message, source, lineno, colno, error) {
  console.log(message, source, lineno, colno, error)
  console.log(source)
  console.log(lineno)
  console.log(colno)
  console.log(error)
  return true
}

参数:

  1. message:错误信息(字符串)。
  2. source:发生错误的脚本URL(字符串)
  3. lineno:发生错误的行号(数字)
  4. colno:发生错误的列号(数字)
  5. error:Error对象(对象)

除了以上参数,该事件还可以返回true,阻止执行默认事件,控制台不显示error提示。

注意事项 - 跨域

在不同域中的语法发生错误的时候,为避免信息泄露,语法错误的细节将不会报告,而代之简单的"Script error."。

在某些浏览器中,通过<script>标签使用crossorigin属性并要求服务器发送适当的 CORS HTTP 响应头,该行为可被覆盖。

  1. 相关的js文件上加上Access-Control-Allow-Origin:*的response header
  2. 引用相关的js文件时加上crossorigin属性

两种操作缺一不可

window.onerror = function (message, source, lineno, colno, error) {
  if(message == 'Script error.') {
    console.log('js无法访问,请通过浏览器控制台查看详情')
    return false
  }
  console.log(message, source, lineno, colno, error)
  return true
}

若无法避免可以在返回Script error.的情况下告知通过浏览器控制台查看。

资源性能监控-performance.getEntries()

该方法返回所有静态资源的数组列表;每一项是一个请求的相关参数有name,type,时间等等。

与 performance.timing 对比:

  • 没有与 DOM 相关的属性
  • 新增了name、entryType、initiatorType和duration四个属性
    • name表示:资源名称,也是资源的绝对路径,可以通过performance.getEntriesByName(name属性的值),来获取这个资源加载的具体属性
    • entryType表示:资源类型 "resource",还有“navigation”, “mark”, 和 “measure”另外3种
    • initiatorType表示:请求来源 "link",即表示<link>标签,还有script<script>,“img”即<img>标签,“css”比如background的url方式加载资源以及“redirect”即重定向 等。
    • duration表示:加载时间,是一个毫秒数字

getEntriesByName

除了上面获取所有资源请求数据外,还可以使用getEntriesByName()获取特定的资源数据。

window.performance.getEntriesByName(name, type);

  • name的取值对应到资源数据中的name字段
  • type取值对应到资源数据中的entryType字段

附录

第二篇: 前端性能监控实践(二)

github: CHU295