前端网页性能指标分析

本文主要介绍了前端网页性能各项指标,例如首包、FTP、FP、FCP、FMP 等主流的前端性能指标定义与获取。希望对网页性能分析、监控感兴趣的小伙伴有所帮助。

导航计时(Navigation Timing)

一图胜千言

页面加载时间分析
页面加载时间分析

网页加载各阶段计时字段说明

字段描述计算方式备注
unload前一个页面卸载耗时unloadEventEnd – unloadEventStart前一个页面卸载时可能监听了 unload 做些数据收集,会影响页面跳转
redirect重定向耗时redirectEnd – redirectStart过多重定向影响性能
appCache缓存耗时domainLookupStart – fetchStart
dnsDNS 解析耗时domainLookupEnd – domainLookupStart
tcpTCP 连接耗时connectEnd – connectStart
sslSSL 安全连接耗时connectEnd – secureConnectionStart只在 HTTPS 下有效
ttfbTime to First Byte(TTFB),网络请求耗时responseStart – requestStart
response数据传输耗时responseEnd – responseStart
dom可交互 DOM 解析耗时domInteractive – responseEndInteractive content
dom2剩余 DOM 解析耗时domContentLoadedEventStart – domInteractiveDOMContentLoaded 所有DOM元素都加载完毕(除了 async script)
DCLDOMContentLoaded 事件耗时domContentLoadedEventEnd – domContentLoadedEventStartdocument.addEventListener(‘DOMContentLoaded’, cb)
resources资源加载耗时loadEventStart – domContentLoadedEventEnd完整DOM(DOMContentLoaded)到资源加载完毕(window.onLoad)时间
onLoadonLoad事件耗时loadEventEnd – loadEventStart

传统自定义关键性能指标

字段描述计算方式备注
firstbyte首包时间responseStart – domainLookupStart
fptFirst Paint Time, 首次渲染时间 / 白屏时间responseEnd – fetchStart从请求开始到浏览器开始解析第一批 HTML 文档字节的时间差
ttiTime to Interact,首次可交互时间domInteractive – fetchStart浏览器完成所有 HTML 解析并且完成 DOM 构建,此时浏览器开始加载资源
readyHTML 加载完成时间, 即 DOM Ready 时间domContentLoadedEventEnd – fetchStart如果页面有同步执行的 JS,则同步 JS 执行时间 = ready – tti
load页面完全加载时间loadEventStart – fetchStartload = 首次渲染时间 + DOM 解析耗时 + 同步 JS 执行 + 资源加载耗时

格外提一下小程序

字段描述计算方式备注
fptFirst Paint Time, 首次渲染时间onShow (first page) – onLaunch (app)小程序从 onLaunch 到第一个页面 onShow 之间的时间

计时 API

传统 Navigation Timing API

使用方法

计算主页面

JavaScript
const t = performance.timing;

const pageloadtime = t.loadEventStart - t.navigationStart,
  dns = t.domainLookupEnd - t.domainLookupStart,
  tcp = t.connectEnd - t.connectStart,
  ttfb = t.responseStart - t.navigationStart;

计算页面资源

JavaScript
const r0 = performance.getEntriesByType('resource')[0];

const loadtime = r0.duration,
  dns = r0.domainLookupEnd - r0.domainLookupStart,
  tcp = r0.connectEnd - r0.connectStart,
  ttfb = r0.responseStart - r0.startTime;

注意事项

1、计算HTML文档请求使用 Nav Timing

获取主页 html 数据,应该使用 performance.timing,而不是 performance.getEntriesByType(‘resource’)[0]。

performance.getEntriesByType(‘resource’) 表示当前 HTML 文档中引用的所有静态资源信息,不包括本身 HTML 信息。

如果当前不包含任何静态资源那么 performance.getEntriesByType(‘resource’) === [] 使用 [0].xx 会报错。

2、计算静态资源使用 getEntriesByType(‘resource’) 代替 getEntries()

getEntries() 包含以下六种类型

  1. navigation
  2. resource
  3. mark
  4. measure
  5. paint
  6. frame

在比较老的浏览器中,getEntries() 通常情况下一般只有 resource 类型等同于 getEntriesByType(‘resource’)。
因为 navigation 是 Navigation Timing 2 规范,老的浏览器不支持。而 mark 和 measure 是 User Timing 用户自定义类型。
最后两个对于目前(2020年) 来说实现的浏览器就更少了。

所有使用 getEntries() 来检索静态资源都需要过滤其他几种类型,getEntriesByType(‘resource’) 就很明确。

3、secureConnectionStart 问题

secureConnectionStart 用来测量 SSL 协商 所花费的时间,可能有三种值

  1. undefined,浏览器不支持该属性;
  2. 0,未使用 HTTPS;
  3. timestamp 时间戳,使用了 HTTPS

chrome 很老的版本有一个 bug,当获取资源复用了已建立的 HTTPS 信道时,secureConnectionStart 设置为 0 了,按标准应该设置为时间戳。

取值时应该避免不支持和未使用的情况

JavaScript
const r0 = performance.getEntriesByType('resource')[0];
if ( r0.secureConnectionStart ) {
  const ssl = r0.connectEnd - r0.secureConnectionStart;
}

4、跨域资源设置响应头 Timing-Allow-Origin

获取页面资源时间详情时,有跨域的限制。默认情况下,跨域资源以下属性会被设置为 0

  • redirectStart、redirectEnd
  • domainLookupStart、domainLookupEnd
  • connectStart、connectEnd
  • secureConnectionStart
  • requestStart
  • responseStart

对于可控跨域资源例如自家 CDN,Timing-Allow-Origin 的响应头 origins 至少得设置了主页面的域名,允许获取资源时间。

一般对外公共资源设置为 Timing-Allow-Origin: *。

对于第三方不可控资源且未设置 Timing-Allow-Origin 头,应该过滤掉这些无效数据。

如果未正确设置 Timing-Allow-Origin 的话

  1. 未做过滤,那么上报的数据会极大优于用户实际使用情况;
  2. 做了过滤,那么上了跨域 CDN 的资源也无法上报数据,导致分析不出上了 CDN 的优势。
JavaScript
// Resource Timing
const r0 = performance.getEntriesByType('resource')[0],
  loadtime = r0.duration;

// 只要选取上述一个属性(除了secureConnectionStart)进行判断即可
if ( r0.requestStart ) {
  const dns = r0.domainLookupEnd - r0.domainLookupStart,
    tcp = r0.connectEnd - r0.connectStart,
    ttfb = r0.responseStart - r0.startTime;
}

let ssl = 0; // 默认为 0,当然也可以在数据库层面去做
// 使用了 HTTPS 在计算
if ( r0.secureConnectionStart ) {
  ssl = r0.connectEnd - r0.secureConnectionStart;
}

5、注意属性值为 0 的含义

  1. secureConnectionStart === 0
    • 未使用 HTTPS
  2. 跨域且未设置正确的 Timing-Allow-Origin 时,有若干属性值为 0,参考上述 4
  3. DNS 解析时间 domainLookupEnd – domainLookupStart === 0
    • 和 HTML 同域名下的资源,DNS 时间可能均为 0,因为浏览器会缓存当前解析域名的 IP;
    • 浏览器预解析了 DNS 并缓存,<link rel=”dns-prefetch” href=”//cross-domain.com” />。
  4. TCP 建立连接时间 connectEnd – connectStart === 0
    • 例如浏览器与每台主机大概能同时建立 6 个独立的 TCP 连接,那么头 6 个资源的 TCP 非零,剩余的 keep-alive 信道复用 TCP 时间为 0
  5. SSL connectEnd – secureConnectionStart === 0
    • 与 TCP 相同
    • 未使用 HTTPS

总之,为零有很多场景,注意区分。

  1. 不支持
  2. 未使用
  3. 复用
  4. 缓存
  5. 安全原因不予显示

6、304

很老的 chrome 版本有个 bug,在 200 有 Timing-Allow-Origin 未在 304 时设置,
导致上述很多属性未能设置为时间戳类型而是 0。

那么问题来了

  1. 你在 #4 中过滤了 304 的情况,只统计了 200 的情况,众所周知 304 缓存技术加载速度优于非缓存的 200。这会拉低的你平均统计性能。
  2. 如果不过滤,那又会获得比 304 还优的性能统计。

碰到这种情况暂时就没办法区分了,幸运的是 chrome 在 version 37 时修复了。

提示:iframe 与文档环境是相互隔离的,你可以获取 iframe 的 contentWindow.performance 来获取。

现代计时 API

Performance Navigation Timing

替代 performance.timing 的方案,精度高更准确。(但目前兼容性高,仍然可使用,未来可能被废弃)。

使用方法

JavaScript
const pageNav = performance.getEntriesByType('navigation')[0];

Performance Navigation Timing 使用了High-Resolution Time,时间精度可以达毫秒的小数点好几位。

JSON
{
    "name": "https://developer.mozilla.org/zh-CN/docs/Web/Performance",
    "entryType": "navigation",
    "startTime": 0,
    "duration": 13636.144999996759,
    "initiatorType": "navigation",
    "nextHopProtocol": "h2",
    "workerStart": 0,
    "redirectStart": 0,
    "redirectEnd": 0,
    "fetchStart": 8.684999993420206,
    "domainLookupStart": 8.684999993420206,
    "domainLookupEnd": 8.684999993420206,
    "connectStart": 8.684999993420206,
    "connectEnd": 8.684999993420206,
    "secureConnectionStart": 8.684999993420206,
    "requestStart": 15.749999991385266,
    "responseStart": 10650.364999994054,
    "responseEnd": 13565.22999999288,
    "transferSize": 56666,
    "encodedBodySize": 56127,
    "decodedBodySize": 207120,
    "serverTiming": [],
    "workerTiming": [],
    "unloadEventStart": 10659.469999998691,
    "unloadEventEnd": 10659.5299999899,
    "domInteractive": 13574.969999986934,
    "domContentLoadedEventStart": 13612.624999994296,
    "domContentLoadedEventEnd": 13612.629999988712,
    "domComplete": 13635.66999998875,
    "loadEventStart": 13635.704999993322,
    "loadEventEnd": 13636.144999996759,
    "type": "navigate",
    "redirectCount": 0
}

新增了不少属性,可以获取更加详细的信息(resource 也一样)。

JavaScript
// Service worker 响应时间
let workerTime = 0;
if (pageNav.workerStart > 0) {
  workerTime = pageNav.responseEnd - pageNav.workerStart;
}

// HTTP header 大小
const headerSize = pageNav.transferSize - pageNav.encodedBodySize;

// 压缩比率,如果是 1 的话,也能说明未开启例如 gzip
const compressionRatio = pageNav.decodedBodySize / pageNav.encodedBodySize;

兼容,由于 performance.getEntriesByType(‘navigation’) 取不到并不会报错而是返回空数组。

JavaScript
if (performance.getEntriesByType('navigation').length > 0) {
  // We have Navigation Timing API
}
Performance Paint Timing
现代浏览器渲染重要指标
现代浏览器渲染重要指标

Paint Timing 定义两个重要的新指标:

  1. 首次绘制 (FP,first-paint) ,浏览器渲染任何在视觉上不同于导航前屏幕内容之内容的时间点。这段时间不就是白屏耗时嘛。
  2. 首次内容绘制 (FCP,first-contentful-paint),浏览器渲染来自 DOM 第一位内容的时间点。这段时间不就是灰屏耗时嘛。
JavaScript
// 直接在代码里这么用的话,不一定取得到,需要轮询
performance.getEntriesByType('paint');
JSON
[
  {
    "name": "first-paint",
    "entryType": "paint",
    "startTime": 17718.514999956824,
    "duration": 0
  },
  {
    "name": "first-contentful-paint",
    "entryType": "paint",
    "startTime": 17718.519999994896,
    "duration": 0
  }
]
  • performance.getEntriesByType 返回的是数组,只有准备好的数据才能入组,你可能需要轮询,或找到一个恰当的时间点来上报数据。
    新标准,提供了 PerformanceObserver API 来帮你监听响应的资源数据是否准备好了。
JavaScript
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    // `name` will be either 'first-paint' or 'first-contentful-paint'.
    const metricName = entry.name;
    const time = Math.round(entry.startTime + entry.duration);

    collect({
      name: metricName,
      time: time,
    });
  }
});
observer.observe({entryTypes: ['paint'/* , 'navigation', resource */]});

使用需要做代码兼容

JavaScript
if ('performance' in window) {
  if ('PerformanceObserver' in window) {
    // todo
  } else {
    // todo
  }
}
  • 首次有效绘制 First Meaning Paint (FMP):表示当前页面最想展示给用户的元素渲染的时间点,即主元素渲染点。
  • FMP 没有标准化的定义,需要开发自己定义。例如元素增速最陡峭的那个时间点。
User timing

用户自定义计时

  • performance.mark 打点,参数为点位名称标识
JavaScript
performance.mark('starting_calculations');
const multiply = 82 * 21;
performance.mark('ending_calculations');

performance.mark('starting_awesome_script');
function awesomeScript() {
  console.log('doing awesome stuff');
}
performance.mark('ending_awesome_script');

performance.measure 计算,参数为点位名称标识、mark 点位1、mark 点位2

Diff
performance.mark('starting_calculations');
const multiply = 82 * 21;
performance.mark('ending_calculations');
+ performance.measure('multiply_measure', 'starting_calculations', 'starting_calculations');

performance.mark('starting_awesome_script');
function awesomeScript() {
  console.log('doing awesome stuff');
}
performance.mark('starting_awesome_script');
+ performance.measure('awesome_script', 'starting_awesome_script', 'starting_awesome_script');

取出时间

JavaScript
const measures = performance.getEntriesByType('measure');
measures.forEach(measureItem => {
  console.log(`${measureItem.name}: ${measureItem.duration}`);
});

上报数据

一般可以考虑在用户准备卸载页面时上报,毫无疑问这个时间点不会干扰用户在当前页的操作。
但是如果上报耗时很长,会影响用户跳转到下一页的体验。可以使用 navigator.sendBeacon。

传统解决方案,在 unload 中处理

  1. 因为页面卸载了,就不会关心异步 ajax 的完成接收,所以一般使用同步 ajax 来阻塞页面卸载。
  2. 创建图片,用 img src 来发送请求。
  3. setTimeout(ajax, 0)。

navigator.sendBeacon 解决了以上问题

  1. 页面卸载了,依旧可以异步请求。
  2. 不阻塞当前页的卸载。
  3. 使用简单。

不推荐 unload 处理了,如下写法:

JavaScript
// bad
window.addEventListener('unload', function() {
  // 注意 performance.getEntries 会取当前页所有资源包括页面本身的性能信息
  // 注意 数据体量问题
  let rumData = new FormData();
  rumData.append('entries', JSON.stringify(performance.getEntries()));

  // 是否支持
  if('sendBeacon' in navigator) {
    // Beacon 发起请求
    if(navigator.sendBeacon(endpoint, rumData)) {
      // sendBeacon 发送成功
    } else {
      // sendBeacon 发送失败! 使用 XHR or fetch 代替
    }
  } else {
    // sendBeacon 不支持! 使用 XHR or fetch 代替
  }
}, false);

推荐以下写法,具体原因参考《页面生命周期》

JavaScript
// good
function clear(fn) {
  ['visibilitychange', 'pagehide', 'beforeunload']
    .forEach(event => window.removeEventListener(event, fn, true));
}

function collect() {
  // 注意 performance.getEntries 会取当前页所有资源包括页面本身的性能信息
  // 注意 数据体量问题
  let rumData = new FormData();
  rumData.append('entries', JSON.stringify(performance.getEntries()));
  
  if('sendBeacon' in window.navigator) {
    if( window.navigator.sendBeacon(endpoint, rumData) ) {
      clear(collect);
    } else {
      // 异步发请求失败
    }
  } else {
    // todo 同步 ajax
    clear(collect);
  }
}

const isSafari = typeof safari === 'object' && safari.pushNotification;
const isIE10 = !('onpagehide' in window);

window.addEventListener('visibilitychange', collect, true);
!isIE10 && window.addEventListener('pagehide', collect, true);

if(isSafari || isIE10) {
  window.addEventListener('beforeunload', collect, true);
}

总结

  • Navigation Timing 收集 HTML 文档性能指标。
    • performance.timing 常用、解决兼容性
    • performance.getEntriesByType(‘navigation’)[0] 新标准,精度高内容更详细,兼容性较差
  • Resource Timing 收集 HTML 依赖的资源的性能指标,如 CSS、JS、图片、字体等。
    • performance.getEntriesByType(‘resource’) 新老一样使用,新标准做了扩展。
  • User timing 收集用户自定义
    • performance.getEntriesByType(‘measure’) 可以考虑,用来对 FMP 打点。

参考

  1. HTML DOM标准
  2. W3C Navigation Timing
  3. Navigation Timing Level 2
  4. User Timing Level 2
  5. boomerang
  6. Resource Timing practical tips
  7. Assessing Loading Performance in Real Life with Navigation and Resource Timing
  8. Navigator.sendBeacon
  9. performance-bookmarklet
  10. waterfall.js