cover_image

前端系统性能监控概述

陈川 SQB Blog
2022年09月15日 02:06

编辑:邵一帆、毛宇、赵远景

图片

前言

web 技术目前在各平台大规模被使用,天然的跨 iOS、Android、PC、Mac、Linux 等平台,结合响应式网页设计(Responsive Web design ) ,一次开发,可以快速的支持多平台,在身处各地的广大用户的各类设备中进行访问。这时候就会产生一个问题,用户的设备是多种多样的,设备的系统类型、浏览器版本、处理器性能也不尽相同,如何监测用户打开网页的速度、效率,从而进行有效分析,发现潜在的性能瓶颈,并做针对性的优化成了前端工作当中一个重要的课题

本文将跟大家分享一下如何对前端性能进行监控,并对抽象性的指标进行分析。首先跟大家介绍一下耳熟能详的 performance.timing,通过这个 api 可以获取到页面加载关于网络方面的一些信息。

performance.timing

performance.timing 返回一个 PerformanceTiming 对象,里面的数据为只读的毫秒数,包含了浏览器开始请求服务器到页面被渲染各个节点的时间,经过计算可以得出页面的各类性能指标。

const timing = performance.timing;

在浏览器执行,会返回以下的数据

  • connectEnd
  • connectStart
  • domComplete
  • ...
  • ...
  • secureConnectionStart
  • unloadEventEnd
  • unloadEventStart

将这些数据按照时间线串联起来,可以完整的看到网页的各个时间节点:

图片

再经过计算便可以得到一些需要页面性能指标:

白屏时间

const general = timing.domComplete - timing.navigationStart;

重定向耗时

const redirect = timing.fetchStart - timing.navigationStart;

dns 解析耗时

const dns = timing.domainLookupEnd - timing.domainLookupStart;

tcp 连接耗时

const connect_tcp = timing.connectEnd - timing.connectStart;

发起请求耗时

const request = timing.responseStart - timing.requestStart;

html 下载耗时

const response = timing.responseEnd - timing.responseStart;

头部资源下载耗时

const assets = timing.domComplete - timing.responseEnd;

如果对 performance.timing 数据进行采集、存储、分析可以绘制出可视化的图表,各类指标一目了然。

图片

performance.timing 可以检测一定的网页性能的数据,但是这个 API 目前已经从 web 标准中移除,MDN 网站上是这样描述的:

已弃用: 不再推荐使用该特性。虽然一些浏览器仍然支持它,但也许已从相关的 web 标准中移除,也许正准备移除或出于兼容性而保留。请尽量不要使用该特性,并更新现有的代码。

并且performance.timing只能检测跟网络相关数据,对于一些前端用户交互更为细致的考量鞭长莫及。

图片

但是聪明的人类总会制作出更加精美、抽象、全面的轮子,给我们开发进行使用,我们可以使用 PerformanceObserver 来进行前端性能的监测,下面对这个 API 的使用做一些简单的介绍。

PerformanceObserver

PerformanceObserver 方法被用来响应各类 PerformanceEntry 事件,这些事件包含各类性能指标耗时、时间线。

const observer = new PerformanceObserver((list) => {
  list.getEntries().forEach((entry) => {
    console.log(
      `name: ${entry.name}, type: ${entry.entryType}, start: ${entry.startTime}, duration: ${entry.duration}`
    );
  });
});
observer.observe({ entryTypes: ["mark""measure"] });
performance.mark("registered-observer");

浏览器会打印以下的数据:

name: registered-observer, type: mark, start: 4254.199999999255, duration: 0
name: routeChange, type: mark, start: 6358.5999999996275, duration: 0
name: beforeRender, type: mark, start: 6406, duration: 0
name: Next.js-route-change-to-render, type: measure, start: 6358.5999999996275, duration: 47.40000000037253
name: afterRender, type: mark, start: 6409.0999999996275, duration: 0

PerformanceEntry.entryTypes

PerformanceEntry.entryTypes 包含的值的解释:

属性名属性的类型属性的描述
elementstring汇报元素加载的耗时
navigationURL页面的地址
resourceURL资源解析的地址
markstring用来标记调用 performance.mark()的名字
measurestring用来标记调用 performance.measure()的名字
paintstring'first-paint' 或者 'first-contentful-paint'
longtaskstring汇报长任务实例

通过 PerformanceObserver 方法取到这些参数、对应的值,就可以对页面各维度性能指标数据进行抽象性的分析了,下面一一列举几个比较重要的指标,并对这些指标的含义、原理、如何采集做出简要的解释。

TTFB

TTFB: Time to First Byte 首字节加载耗时

TTFB 是检验服务器响应的一个关键性指标,并记录了从浏览器发起请求到浏览器接收到首字节的时间,能直接反映出服务器响应速度。如下图所示,包括重定向耗时、缓存(如果有)、DNS 解析、TCP 连接耗时以及请求耗时。

图片

汇总来说包括以下部分:

  1. 重定向耗时
  2. 缓存(如果有)
  3. DNS 解析
  4. TCP 连接耗时
  5. 请求耗时,直到 response 返回第一个字节

由于 performance.timing 惨遭废弃,相关的数据可以通过这个 API 进行获取。

如何采集 TTFB 数据

const observer = new PerformanceObserver((list) => {
  const [pageNav] = list.getEntriesByType("navigation");
  console.log(`TTFB: ${pageNav.responseStart}`);
});

observer.observe({ entryTypes: ["navigation"] });

浏览器会打印出如下数据:

TTFB: 10.200000001117587

TTFB 的值越小越好,小于 800 毫秒为优秀,大于 1800 毫秒为糟糕。

图片

FCP

FCP: First Contentful Paint 首次内容绘制 DOM

FCP 表示页面从开始加载到页面中某些元素(比如:图片、文字等)在浏览器中渲染出来的耗时,通过示意图可以较为清晰的理解这个概念性指标

图片

如何采集 FCP

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    const metricName = entry.name;
    const time = Math.round(entry.startTime + entry.duration);
    console.log(metricName, time);
  }
});

observer.observe({ entryTypes: ["paint"] });

在网页中执行会有如下数据的打印

first-paint 1139
first-contentful-paint 1139
图片

FCP 在 1.8s 以下为优秀,1.8s-3.0s 为一般,3.0s 以上为较差,这个指标通过直觉判断也能有直观的感受,1.8 秒以内页面页面展示出内容,可以被称为秒开了。在控制台中也可以看到 first-paint 这个数据,这个数据不如 FCP 具备很强参考性,所以这里不做更多分析。

LCP

LCP: Largest Contentful Paint 最大内容绘制

上面讲到 FCP,你也许会注意到,部分内容被渲染,还存在其他一些的内容没有被渲染,这对用户访问网页还是存在一定的困扰,所以页面加载完成也需要进行监测。

通过下面的图片可以直观的看到 FPC 跟 LCP 的区别

图片

如何采集 LCP 数据

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log("LCP: ", entry.startTime);
  }
});
observer.observe({ entryTypes: ["largest-contentful-paint"] });

控制台会有如下的打印:

LCP: 99.099
图片

LCP 小于 2.5 秒为优秀,大于 4.0 秒为糟糕

FID

FID: First Input Delay 首次输入延迟

用户对于访问打开速度的感觉可以通过 LCP 来衡量,用户看到页面之后,变会对页面进行一些操作,用户需要等待多久可以操作页面,这也是一个重要的数据,用户的时间很宝贵,我们耽误不起,用户操作了页面,但是没反应,这是一个非常令人难以接受的事情。

如何采集 FID 数据

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    const time = entry.processingStart - entry.startTime;
    console.log(entry.entryType, time);
  }
});
observer.observe({ entryTypes: ["first-input"] });

function onClick({
  console.log("click");
}
<button click="onClick(this)">button</button>

控制台会有如下打印:

first-input 4.2000000001862645

FID 的数值越小越好,最好在 100ms 以下,这样用户看到页面展示之后,再点下去的那一下,页面的 js 脚本就已经执行好,可以响应用户的操作。

图片

CLS

CLS: Cumulative Layout Shift 累积布局偏移

当用户对页面进行操作的时候,页面维持一个稳定的状态,不抖动、不错位这是一个基本的要求,如果页面发生大范围的抖动,用户可能会错误的点击到其他地方,产生错误的操作,使用户的感受很糟糕。

布局偏移分数

布局偏移分数 = 影响分数 * 距离分数

距离分数定义

不稳定的元素相对于视口(viewport)在水平或者垂直方向上发生了位移,前后偏移的量相对于视口(viewport)的百分比。下图可以看到内容部分相对于视口(viewport)偏移了 25%,可以得到距离分数为 0.25。

图片

影响分数定义

不稳定元素发生位移的前后两帧占据视口(viewport)的比例,在下面的图片中,我们可以看到内容开始占据一半的屏幕,发生位移后往下移动了 25%,这样前后加起来就是移动了 75%,可以得到影响分数为 0.75。

图片

计算

0.75 * 0.25 = 0.1875

这个例子中的内容部分移动的距离太过于大,显然用户不能得到一个好的体验。CLS 显然越小越好,小于 0.1 算优,大与 0.25 为很差。

图片

如何采集

通过 layout-shift 这个 entryTypes 可以对 CLS 进行一些采集

let clsValue = 0;
let clsEntries = [];

let sessionValue = 0;
let sessionEntries = [];

new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    if (!entry.hadRecentInput) {
      const firstSessionEntry = sessionEntries[0];
      const lastSessionEntry = sessionEntries[sessionEntries.length - 1];

      if (
        sessionValue &&
        entry.startTime - lastSessionEntry.startTime < 1000 &&
        entry.startTime - firstSessionEntry.startTime < 5000
      ) {
        sessionValue += entry.value;
        sessionEntries.push(entry);
      } else {
        sessionValue = entry.value;
        sessionEntries = [entry];
      }

      if (sessionValue > clsValue) {
        clsValue = sessionValue;
        clsEntries = sessionEntries;

        console.log("CLS:", clsValue, clsEntries);
      }
    }
  }
}).observe({ type"layout-shift"bufferedtrue });

setTimeout(() => {
  document.querySelector("div").style.marginTop = "25vh";
}, 1000);
<div
  style="
        width: 100vw;
        height: 50vh;
        background-color: beige;
        display: inline-flex;
        justify-content: center;
        align-items: center;
      "

>

  <img style="width: 150px" src="xxx.gif" />
</div>

关于 CLS 更多的信息可以参考 layout-instability[1]

总结

本文详细的介绍了如何采用 performance API 和 PerformanceObserver API 进行前端性能监控,然后从服务器、首屏渲染、用户初次可交互等角度,将指标数据进行统计分析,得出页面性能优化方案。随着收钱吧业务复杂度的提升,目前公司在涉及到交易、对账、提现等前端项目上,对网页性能有着极高的要求。降低用户对页面响应延迟容忍度,让用户获得最佳体验感,对于前端开发人员来说至关重要。目前团队从各个角度切入、已做出针对性的优化,实践下来得到了不少有用的经验。下一期将会跟大家分享如何结合本文所介绍抽象性的指标,对网页的性能做针对性的优化。

关于作者

陈川,来自基础业务开发部

参考资料

[1]

A proposal for a Layout Instability specification: https://github.com/WICG/layout-instability

图片

继续滑动看下一个
SQB Blog
向上滑动看下一个