Web优化相关,前端性能监控工具

  • 关注性能是工程师的本性 + 本分;
  • 页面性能对用户体验而言十分关键。每次重构对页面性能的提高,仅靠工程师开发设备的测试数据是没有说服力的,须要有大量的真实数据用于验证;
  • 资源挂了、加载出现异常,不能总靠用户投诉才后知后觉,须要主动报警。

用什么监控

关于前端性能指标,W3C 定义了强大的 Performance API,其中又包括了 High Resolution TimeFrame TimingNavigation TimingPerformance TimelineResource TimingUser Timing 等诸多具体标准。前端

本文主要涉及 Navigation Timing 以及 Resource Timing。截至到 2018 年中旬,各大主流浏览器均已完成了基础实现。git

Navigation Timing Support

 

 

Resource Timing Support
 

Performance API 功能众多,其中一项,就是将页面自身以及页面中各个资源的性能表现(时间细节)记录了下来。而咱们要作的就是查询和使用。github

读者能够直接在浏览器控制台中输入 performance ,查看相关 API。web

接下来,咱们将使用浏览器提供的 window.performance 对象(Performance API 的具体实现),来实现一个简易的前端性能监控工具。数组

5 分钟撸一个前端性能监控工具

第一行代码

将工具命名为 pMonitor,含义是 performance monitor浏览器

const pMonitor = {}

监控哪些指标

既然是“5 分钟实现一个 xxx”系列,那么就要有取舍。所以,本文只挑选了最为重要的两个指标进行监控:缓存

  • 页面加载时间
  • 资源请求时间

页面加载

有关页面加载的性能指标,能够在 Navigation Timing 中找到。Navigation Timing 包括了从请求页面起,到页面完成加载为止,各个环节的时间明细。app

能够经过如下方式获取 Navigation Timing 的具体内容:dom

const navTimes = performance.getEntriesByType('navigation')

返回结果是一个数组,其中的元素结构以下所示:异步

{
  "connectEnd": 64.15495765894057,
  "connectStart": 64.15495765894057,
  "domainLookupEnd": 64.15495765894057,
  "domainLookupStart": 64.15495765894057,
  "domComplete": 2002.5385066728431,
  "domContentLoadedEventEnd": 2001.7384263440083,
  "domContentLoadedEventStart": 2001.2386167400286,
  "domInteractive": 1988.638474368076,
  "domLoading": 271.75174283737226,
  "duration": 2002.9385468372606,
  "entryType": "navigation",
  "fetchStart": 64.15495765894057,
  "loadEventEnd": 2002.9385468372606,
  "loadEventStart": 2002.7383663540235,
  "name": "document",
  "navigationStart": 0,
  "redirectCount": 0,
  "redirectEnd": 0,
  "redirectStart": 0,
  "requestStart": 65.28225608537441,
  "responseEnd": 1988.283025689508,
  "responseStart": 271.75174283737226,
  "startTime": 0,
  "type": "navigate",
  "unloadEventEnd": 0,
  "unloadEventStart": 0,
  "workerStart": 0.9636893776343863
}

关于各个字段的时间含义,Navigation Timing Level 2 给出了详细说明:

 

Navigation Timing attributes

 

不难看出,细节满满。所以,可以计算的内容十分丰富,例如 DNS 查询时间,TLS 握手时间等等。能够说,只有想不到,没有作不到~

既然咱们关注的是页面加载,那天然要读取 domComplete:

const [{ domComplete }] = performance.getEntriesByType('navigation')

定义个方法,获取 domComplete

pMonitor.getLoadTime = () => {
  const [{ domComplete }] = performance.getEntriesByType('navigation')
  return domComplete
}

到此,咱们得到了准确的页面加载时间。

 

资源加载

既然页面有对应的 Navigation Timing,那静态资源是否是也有对应的 Timing 呢?

答案是确定的,其名为 Resource Timing。它包含了页面中各个资源从发送请求起,到完成加载为止,各个环节的时间细节,和 Navigation Timing 十分相似。

获取资源加载时间的关键字为 'resource', 具体方式以下:

performance.getEntriesByType('resource')

不难联想,返回结果一般是一个很长的数组,由于包含了页面上全部资源的加载信息。

每条信息的具体结构为:

{
  "connectEnd": 462.95008929525244,
  "connectStart": 462.95008929525244,
  "domainLookupEnd": 462.95008929525244,
  "domainLookupStart": 462.95008929525244,
  "duration": 0.9620853673520173,
  "entryType": "resource",
  "fetchStart": 462.95008929525244,
  "initiatorType": "img",
  "name": "https://cn.bing.com/sa/simg/SharedSpriteDesktopRewards_022118.png",
  "nextHopProtocol": "",
  "redirectEnd": 0,
  "redirectStart": 0,
  "requestStart": 463.91217466260445,
  "responseEnd": 463.91217466260445,
  "responseStart": 463.91217466260445,
  "startTime": 462.95008929525244,
  "workerStart": 0
}

咱们关注的是资源加载的耗时状况,能够经过以下形式得到:

const [{ startTime, responseEnd }] = performance.getEntriesByType('resource')
const loadTime = responseEnd - startTime

Navigation Timing 类似,关于 startTimefetchStartconnectStartrequestStart 的区别, Resource Timing Level 2 给出了详细说明:

 

Resource Timing attributes

 

并不是全部的资源加载时间都须要关注,重点仍是加载过慢的部分。

出于简化考虑,定义 10s 为超时界限,那么获取超时资源的方法以下:

const SEC = 1000
const TIMEOUT = 10 * SEC
const setTime = (limit = TIMEOUT) => time => time >= limit
const isTimeout = setTime()
const getLoadTime = ({ startTime, responseEnd }) => responseEnd - startTime
const getName = ({ name }) => name
const resourceTimes = performance.getEntriesByType('resource')
const getTimeoutRes = resourceTimes
  .filter(item => isTimeout(getLoadTime(item)))
  .map(getName)

这样一来,咱们获取了全部超时的资源列表。

简单封装一下:

const SEC = 1000
const TIMEOUT = 10 * SEC
const setTime = (limit = TIMEOUT) => time => time >= limit
const getLoadTime = ({ requestStart, responseEnd }) =>
  responseEnd - requestStart
const getName = ({ name }) => name
pMonitor.getTimeoutRes = (limit = TIMEOUT) => {
  const isTimeout = setTime(limit)
  const resourceTimes = performance.getEntriesByType('resource')
  return resourceTimes.filter(item => isTimeout(getLoadTime(item))).map(getName)
}

上报数据

获取数据以后,须要向服务端上报:

// 生成表单数据
const convert2FormData = (data = {}) =>
  Object.entries(data).reduce((last, [key, value]) => {
    if (Array.isArray(value)) {
      return value.reduce((lastResult, item) => {
        lastResult.append(`${key}[]`, item)
        return lastResult
      }, last)
    }
    last.append(key, value)
    return last
  }, new FormData())
// 拼接 GET 时的url
const makeItStr = (data = {}) =>
  Object.entries(data)
    .map(([k, v]) => `${k}=${v}`)
    .join('&')
// 上报数据
pMonitor.log = (url, data = {}, type = 'POST') => {
  const method = type.toLowerCase()
  const urlToUse = method === 'get' ? `${url}?${makeItStr(data)}` : url
  const body = method === 'get' ? {} : { body: convert2FormData(data) }
  const option = {
    method,
    ...body
  }
  fetch(urlToUse, option).catch(e => console.log(e))
}

回过头来初始化

数据上传的 url、超时时间等细节,因项目而异,因此须要提供一个初始化的方法:

// 缓存配置
let config = {}
/**
 * @param {object} option
 * @param {string} option.url 页面加载数据的上报地址
 * @param {string} option.timeoutUrl 页面资源超时的上报地址
 * @param {string=} [option.method='POST'] 请求方式
 * @param {number=} [option.timeout=10000]
 */
pMonitor.init = option => {
  const { url, timeoutUrl, method = 'POST', timeout = 10000 } = option
  config = {
    url,
    timeoutUrl,
    method,
    timeout
  }
  // 绑定事件 用于触发上报数据
  pMonitor.bindEvent()
}

什么时候触发

性能监控只是辅助功能,不该阻塞页面加载,所以只有当页面完成加载后,咱们才进行数据获取和上报(实际上,页面加载完成前也获取不到必要信息):

// 封装一个上报两项核心数据的方法
pMonitor.logPackage = () => {
  const { url, timeoutUrl, method } = config
  const domComplete = pMonitor.getLoadTime()
  const timeoutRes = pMonitor.getTimeoutRes(config.timeout)
  // 上报页面加载时间
  pMonitor.log(url, { domeComplete }, method)
  if (timeoutRes.length) {
    pMonitor.log(
      timeoutUrl,
      {
        timeoutRes
      },
      method
    )
  }
}
// 事件绑定
pMonitor.bindEvent = () => {
  const oldOnload = window.onload
  window.onload = e => {
    if (oldOnload && typeof oldOnload === 'function') {
      oldOnload(e)
    }
    // 尽可能不影响页面主线程
    if (window.requestIdleCallback) {
      window.requestIdleCallback(pMonitor.logPackage)
    } else {
      setTimeout(pMonitor.logPackage)
    }
  }
}

汇总

到此为止,一个完整的前端性能监控工具就完成了~所有代码以下:

const base = {
  log() {},
  logPackage() {},
  getLoadTime() {},
  getTimeoutRes() {},
  bindEvent() {},
  init() {}
}

const pm = (function() {
  // 向前兼容
  if (!window.performance) return base
  const pMonitor = { ...base }
  let config = {}
  const SEC = 1000
  const TIMEOUT = 10 * SEC
  const setTime = (limit = TIMEOUT) => time => time >= limit
  const getLoadTime = ({ startTime, responseEnd }) => responseEnd - startTime
  const getName = ({ name }) => name
  // 生成表单数据
  const convert2FormData = (data = {}) =>
    Object.entries(data).reduce((last, [key, value]) => {
      if (Array.isArray(value)) {
        return value.reduce((lastResult, item) => {
          lastResult.append(`${key}[]`, item)
          return lastResult
        }, last)
      }
      last.append(key, value)
      return last
    }, new FormData())
  // 拼接 GET 时的url
  const makeItStr = (data = {}) =>
    Object.entries(data)
      .map(([k, v]) => `${k}=${v}`)
      .join('&')
  pMonitor.getLoadTime = () => {
    const [{ domComplete }] = performance.getEntriesByType('navigation')
    return domComplete
  }
  pMonitor.getTimeoutRes = (limit = TIMEOUT) => {
    const isTimeout = setTime(limit)
    const resourceTimes = performance.getEntriesByType('resource')
    return resourceTimes
      .filter(item => isTimeout(getLoadTime(item)))
      .map(getName)
  }
  // 上报数据
  pMonitor.log = (url, data = {}, type = 'POST') => {
    const method = type.toLowerCase()
    const urlToUse = method === 'get' ? `${url}?${makeItStr(data)}` : url
    const body = method === 'get' ? {} : { body: convert2FormData(data) }
    const init = {
      method,
      ...body
    }
    fetch(urlToUse, init).catch(e => console.log(e))
  }
  // 封装一个上报两项核心数据的方法
  pMonitor.logPackage = () => {
    const { url, timeoutUrl, method } = config
    const domComplete = pMonitor.getLoadTime()
    const timeoutRes = pMonitor.getTimeoutRes(config.timeout)
    // 上报页面加载时间
    pMonitor.log(url, { domeComplete }, method)
    if (timeoutRes.length) {
      pMonitor.log(
        timeoutUrl,
        {
          timeoutRes
        },
        method
      )
    }
  }
  // 事件绑定
  pMonitor.bindEvent = () => {
    const oldOnload = window.onload
    window.onload = e => {
      if (oldOnload && typeof oldOnload === 'function') {
        oldOnload(e)
      }
      // 尽可能不影响页面主线程
      if (window.requestIdleCallback) {
        window.requestIdleCallback(pMonitor.logPackage)
      } else {
        setTimeout(pMonitor.logPackage)
      }
    }
  }

  /**
   * @param {object} option
   * @param {string} option.url 页面加载数据的上报地址
   * @param {string} option.timeoutUrl 页面资源超时的上报地址
   * @param {string=} [option.method='POST'] 请求方式
   * @param {number=} [option.timeout=10000]
   */
  pMonitor.init = option => {
    const { url, timeoutUrl, method = 'POST', timeout = 10000 } = option
    config = {
      url,
      timeoutUrl,
      method,
      timeout
    }
    // 绑定事件 用于触发上报数据
    pMonitor.bindEvent()
  }

  return pMonitor
})()

export default pm

调用

若是想追求极致的话,在页面加载时,监测工具不该该占用主线程的 JavaScript 解析时间。所以,最好在页面触发 onload 事件后,采用异步加载的方式:

// 在项目的入口文件的底部
const log = async () => {
  const pMonitor = await import('/path/to/pMonitor.js')
  pMonitor.init({ url: 'xxx', timeoutUrl: 'xxxx' })
  pMonitor.logPackage()
  // 能够进一步将 bindEvent 方法从源码中删除
}
const oldOnload = window.onload
window.onload = e => {
  if (oldOnload && typeof oldOnload === 'string') {
    oldOnload(e)
  }
  // 尽可能不影响页面主线程
  if (window.requestIdleCallback) {
    window.requestIdleCallback(log)
  } else {
    setTimeout(log)
  }
}

设置报警

既能够是每一个项目对应不一样的上报 url,也能够是统一的一套 url,项目分配惟一 id 做为区分。

当超时次数在规定时间内超过约定的阈值时,邮件/短信通知开发人员。

相关文章
相关标签/搜索