灏天阁

前端性能的重要指标

· Yin灏

网站监控,前后端所负责的工作各有不同。

服务器端主要负责分析、告警:

  1. 日志接收处理: 建设后端应用,提供日志上报接口给采集 SDK
  2. 数据发布:后端接受日志后处理成可被实时流计算的数据:dataHub/sls/Kafaka
  3. 日志处理:基于 Flink/spark/storm
  4. 监控告警:前台应用或者机器人,实现监控、告警 通常的服务:应用服务器 + DataHub + Flink + Hologres(RDS)

前端主要负责两个部分:日志采集和日志上报。

日志采集则分为两个层面:页面稳定性和页面流畅性。

页面稳定性

其中很重要的部分是前端的异常,有 JS 报错、接口异常、资源异常等,这些已经在前端异常捕获与处理中有所整理,可以直接从里面 copy 代码拿来用。

还有就是页面白屏与页面崩溃。

页面白屏

方案 实现 优点 缺点
基于 Native 容器 页面加载完成后 3s 页面还是全屏白色像素 不依赖 js,不丢点 依赖容器
基于 PaitingTiming 页面加载完成后 3s 页面没有 first-paint 实现简单,不依赖容器,对页面性能几乎没有影响 兼容性差
基于 MutationObserver 页面加载完成后 3s 是否有节点变化 实现简单,不依赖容器 节点没有变化不代表白屏,可能样式是有的

页面崩溃

方案 实现 优点 缺点
基于 Native 容器 监控 WebView 进程状态,发送 Crash 日志 不依赖 js,不丢点 依赖容器
基于 Service Worker HTML 请求进入 SW 后标记页面开始加载,页面每隔一定时间向 SW 发送一次心跳,一段时间没有收到心跳则认为页面 Crash 不依赖容器 兼容性差。SW 侵入性强,风险高;页面 Pause 后无法进行心跳检测
基于 LocalStorage 里的页面离开状态 在页面加载时标记开始加载,在页面 pagehide/beforeunload 时标记离开,二次进入页面时判断是否正常离开 不依赖容器 ,兼容性较好 埋点发送滞后,起不到监控告警作用

Navigator.sendBeacon这个可以试试。不丢埋点,也不会延迟页面卸载,就是会有兼容性问题。

页面流畅性

加载性能

image.png

指标 采集方法
load NavigationTiming
FCP PerformancePaintTiming
LCP PerformancePaintTiming
FID first input delay PerformanceEventTiming
TTI time to interactive 没有 API,lighthouse 检测
TBT total blocking time 没有 API,lighthouse 检测
Cumulative layout shift Layout Instability API

也存在懒加载的情况,这些指标也不能反映真实的加载性能。要做这块内容现阶段只能侵入性主动埋点。

首屏绘制(First Paint,FP)

浏览器第一次发生变化的时间 浏览器从响应用户输入网址地址,到浏览器开始显示内容的时间。包括了任何用户自定义的背景绘制,它是首先将像素绘制到屏幕的时刻。

首屏内容绘制(First Contentful Paint,FCP)

浏览器从响应用户输入网络地址,浏览器将第一个 DOM 渲染到屏幕的时间。在页面首次绘制文本,图片(包括背景图)、非白色的 canvas 或者 SVG 才算做 FCP。

可交互时间(Time to Interactive,TTI)

网页第一次完全达到可交互状态(可以响应按钮的点击或在文本框输入文字等)的时间点。此时主线程已经达到“流畅”的程度,主线程的任务均不超过 50 毫秒。在一般的管理系统中,TTI 是一个很重要的指标。

首次有效绘制(First Meaning Paint, FMP)

页面的“主要内容”开始出现在屏幕上的时间点,它以前是我们测量用户加载体验的主要指标。本质上是通过一个算法来猜测某个时间点可能是 FMP,但是最好的情况也只有 77%的准确率,在 lighthouse6.0 的时候废弃掉了这个指标,取而代之的是 LCP 这个指标。

最大内容绘制(Largest Contentful Paint,LCP)

可视区“内容”最大的可见元素开始出现在屏幕上的时间点。LCP 也不是完美的,也很容易出错,它会在用户进行交互后就停止捕获,可能会获取到错误的结果,如果有占据页面很大的轮播图也会产生问题会不断的更新 LCP。

// 浏览器访问最初的时间测量点
performance.navigationStart
// navigationStart 至当前的毫秒数
performance.now()
// 各个关键时间点 包括上面的 navigationStart
performance.timing
// 各个资源请求的时间统计信息
performance.getEntries()

// 计算页面白屏时间
performance.getEntriesByType("paint")
// body 标签之前获取当前时间 - performance.timing.navigationStart
// 白屏时间 = 页面开始展示的时间点 - 开始请求的时间点。

// FCP 首屏内容绘制
const fcp_time_by_interactive = performance.timing.interactive - performance.timing.fetchStart
const fcp_time_by_loadEventEnd = = performance.timing.loadEventEnd - performance.timing.navigationStart // 首屏内容渲染结束时间点 - 开始请求的时间点
// 在需要展示的元素页面之前获取当前时间 - performance.timing.navigationStart

// TTI 可交互时间
const tti = domContentLoadedEventEnd - navigationStart

// LCP 最大内容绘制
// 直接使用 PerformanceObserver 来捕获 LCP
const observer = new PerformanceObserver((entryList) => {
  const entries = entryList.getEntries();
  const lastEntry = entries[entries.length - 1];
  const lcp = lastEntry.renderTime || lastEntry.loadTime;
  console.log('LCP:', lcp);
});
observer.observe({ entryTypes: ['largest-contentful-paint'] });
// 或者用库来做
import { getLCP } from 'web-vitals';

// Measure and log the current LCP value,
// any time it's ready to be reported.
getLCP(console.log);

自定义性能采集

指标 采集方法 说明
UserTiming PerformanceUserTiming 业务在首屏渲染完成的时候通过performance.mark(...)进行标记,而采集 SDK 可以通过PerformanceObserver进行采集上报
ElementTiming PerformanceElementTiming UserTiming 的问题是业务同学无法知道首屏的 HTML 创建之后,里面的内容什么时候真正被浏览器渲染出来,而 ElementTiming 则是给到用户一个检测某个文本或者图片内容渲染完成的能力,只需要在特定节点上加 elementtiming 属性就行,这样我们就把一些有代表性的节点渲染完成作为首屏渲染完成的象征。而采集 SDK 可以通过PerformanceObserver进行采集上报

响应速度

从用户操作到页面响应的耗时,通常要求小于 100ms

// 基于 PerformanceEventTiming 监听用户任意输入到浏览器响应的延时时间
var observer = new PerformanceObserver((list) => {
  list.getEntries().forEach((entry) => {
    // name: entry.name
    // 整体耗时: entry.duration
    // 事件处理函数耗时:entry.processingEnd - entry.processingStart
  });
});
observer.observe({ type: "event", buffered: true });

动画流畅性

监听页面上任意动画的帧率帧数是否稳定

在动画运行期间监听每次 requestAnimationFrame 的执行,计算:

  1. 帧率 动画运行帧数/动画运行时间
  2. 掉帧率 (60FPS 标准应该运行的帧数 - 实际运行帧数)/60FPS 标准应该运行的帧数
// 以 60 帧每秒,每一帧之间的间隔
const msInOneFrame = 1000 / 60;
const expectedFrames = Math.floor((e.elapsedTime * 1000) / msInOneFrame);
const error_rate = (expectedFrames - 运行帧数) / expectedFrames;

日志上报

前端异常捕获与处理 这篇文章里提到的触发异常的 hooks,还有上文提到的浏览器的一些 hooks 可以用。

参考资料

- Book Lists -