前端监控 SDK 开发 | 青训营笔记

101 阅读5分钟

这是我参与「第五届青训营」伴学笔记创作活动的第 5 天

0x00 什么是前端监控

前端监控是尽可能地采集浏览器从输入 URL 到页面展示这一过程以及之后用户交互中产出地性能指标与发生的异常事件并上报到平台完成消费,帮助开发者更快地对质量差的页面进行分析与归因。

0x01 为什么需要前端监控

  • 可以帮助用户减少感知等待时间,提高搜索引擎浏览以及用户注册量等

  • 可以减少页面平均加载事件,提高转化次数,降低跳出率

  • 可以用来解决分析问题,比如网站运行速度慢,网站卡顿,图源消失等等问题。

0x02 前端监控什么

  • 性能指标

  • 异常事件

  • 用户行为

部分重要指标

  • 可交互性:卡顿监控、请求性能

  • 服务稳定性:请求成功率

  • 加载速度:首屏性能

  • 可用性:渲染错误/白屏、请求错误、JS 错误、静态资源错误

  • 业务数据:PV/UV、自定义事件

0x03 常用性能指标

传统性能指标

主要依赖 Navigation Timing 或者 Navigation Timing 2,通过记录一个文档从发送请求到加载完毕的各阶段的性能耗时,以加载速度来衡量性能

传统的性能指标专注于容易衡量的技术细节,很难反应用户所真正关心的问题

以用户为中心的性能指标

用户体验

指标

发生了吗?

First Paint(FP) 首次绘制、First Contentful Paint(FCP) 首次内容绘制

内容有用吗?

First Meaningful Paint(FMP) 首次有效绘制、Speed Index(SI) 首屏展现平均值、Largest Contentful Paint (LCP) 最大内容绘制

内容可用吗?

Time to Interactive(TTI) 可交互时间、Total Blocking Time(TBT) 总阻塞时间

令人愉悦吗?

First Input Delay(FID) 首次输入延迟、Cumulative Layout Shift(CLS) 累积布局偏移

First Paint(FP) 首次绘制

首次渲染的时间点,可以视为白屏时间。

First Contentful Paint(FCP)

首次有内容渲染的时间点,指标测量页面从开始加载到页面内容的任何部分在屏幕上完成渲染的时间。

该指标通常与用户无关。

First Meaningful Paint(FMP) 首次有效绘制

首次绘制有意义内容的时间点。

该指标非标准化并且约 20% 的情况下不准确。

Time to Interactive(TTI) 可交互时间

测量页面从开始加载到主要子资源完成渲染,并能够快速、可靠地响应用户输入所需的时间。该指标是反映页面可用性的重要指标,TTI 值越小,代表用户可以更早地操作页面,用户体验就越好。

Speed Index(SI) 首屏展现平均值

衡量页面可视区域加载速度,帮助检测页面的加载体验差异。

该指标检测难度较大,计算密集需要消耗较多资源,不可用于线上监控。

First Input Delay(FID) 首次输入延迟

测量从用户第一次与页面交互直到浏览器对交互作出响应,实际能够开始处理事件时处理程序所经过的时间。

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

最大的内容在可视区域内变得可见的时间点。

该指标容易计算与上报,给出的结果与 FMP 相似。

Total Blocking Time(TBT) 总阻塞时间

量化主线程在空闲之前的繁忙程度,有助于理解在加载期间,页面无法响应用户输入的时间有多久。

若一个任务在主线程上运行超过 50 毫秒,则该任务为一个长任务,超过 50 毫秒后的任务耗时则为任务的阻塞时间,一个页面的 TBT 是从 FCP 到 TTI 之间所有长任务的阻塞时间总和。

Cumulative Layout Shift(CLS) 累积布局偏移

量化在页面加载期间,视口中元素的移动程度。

0x03 常用前端异常

静态资源错误

在拉取和加载静态资源的过程中发生了预期之外的错误,如网络异常等,导致静态资源无法正常渲染到页面上。

JS 错误

在页面运行时发生的 Js 错误会严重影响页面的正常渲染与交互,是前端监控的重点。

白屏异常

白屏异常没有标准化的监听方法,更考验前端监控开发者的功底。

通常可以通过判断 DOM 树的结构来粗略的判断白屏是否发生。

通常导致白屏发生的原因可能有如下几点:

  1. 发生 Js 错误导致关键资源渲染失败。

  2. 请求异常或静态资源加载失败。

  3. 长时间的 Js 线程繁忙阻塞渲染任务。

0x04 性能指标监控

利用 Performance 和 PerformanceObserver 监控标准的渲染数据,优先使用 Performance 来获取。

// 方法一(在页面加载完毕后控制台内输入)
window.performance.getEntriesByType('paint');
window.performance.getEntriesByType('first-input');

// 方法二
const entryTypes = ['paint', 'largest-contentful-paint', 'first-input']

const p = new PerformanceObserver(list => {
  for (const entry of list.getEntries()) {
    console.log(entry);
  }
})
p.observe({ entryTypes });

0x05 JS 错误监控

利用 window.addEventListener 的 error 和 unhandledrejection 可以监控到全局的 JS 错误。

// 1. 监听 js 执行报错
window.addEventListener("error", (e) => {
  // 只有 error 属性不为空的 ErrorEvent 才是一个合法的 js 错误
  if (e.error) {
    console.log('caputure an error', e.error);
  }
});
// throw(new Error('test'));
// 2. 监听 promise rejection
window.addEventListener("unhandledrejection", (e) => {
  console.log('capture a unrejection', e);
});
Promise.reject('test');

0x06 静态资源错误监控

利用 window.addEventListener 的 error 事件可以监控到静态资源错误,注意要和 js error 进行区分。

window.addEventListener('error', e => {
  // 注意区分 js error
  const target = e.target || e.srcElement;
  if (!target) {
    return
  }
  if (target instanceof HTMLElement) {
    let url;
    // 区分 link 标签,获取静态资源地址
    if (target.tagName.toLowerCase() === 'link') {
      url = target.getAttribute('href');
    } else {
      url = target.getAttribute('src');
    }
    console.log('异常的资源', url);
  }
}, true)
const link = document.createElement("link");
link.href = "1.css";
link.rel = "stylesheet";
document.head.append(link);

const script = document.createElement("script");
script.src = "2.js";
document.head.append(script);

0x07 请求异常监控

通过 hook xhr 和 fetch 对象来监听请求时发生的错误。

// 1. 写一个简易的 hook 函数
function hookMethod(
  obj: any,
  key: string,
  hookFunc: Function,
) {
  return (...params: any[]) => {
    obj[key] = hookFunc(obj[key], ...params)
  }
}

// 2. hook xhr 对象的 open 方法拿到请求地址和方法
hookMethod(XMLHttpRequest.prototype, 'open', (origin: Function) =>
  function (method: string, url: string) {
    this.payload = {
      method,
      url,
    };
    // 执行原函数
    origin.apply(this, [method, url]);
  }
)();

// 3. hook xhr 对象的 send 方法监听到错误的请求
hookMethod(XMLHttpRequest.prototype, 'send', (origin: Function) =>
  function (...params: any[]) {
    this.addEventListener("readystatechange", function () {
      if (this.readyState === 4 && this.status >= 400) {
        this.payload.status = this.status;
        console.log(this.payload);
      }
    });
    origin.apply(this, params);
  }
)();

const xhr = new XMLHttpRequest();
xhr.open("post", "111.cc");
xhr.send();