返回首页

性能监控-前端探针设计

布莱克2026-03-31 11:20已编辑
Tip:文章封面与内容无关,作者旅游时拍摄,因为没什么值得把四季都错过!

设计核心:(基于Skywalking)

最小化侵入、自动化采集、全链路串联

采集插件层 (Plugins)

  • Performance Plugin:负责“快不快”。
  • Error Plugin:负责“坏没坏”。
  • Http Plugin:负责“通不通”。

传输层 (Reporter)

  • Buffer (数据缓冲):不会实时发请求,而是将数据暂存在内存队列中。
  • Async Dispatcher (异步上报):利用浏览器的空闲时间发送数据

错误监控模块

JS 运行时错误: 通过 window.onerror 拦截。它能获取错误消息、堆栈(Stack Trace)、所在行列号。

Promise 异常: 现代前端的死穴。通过 unhandledrejection 捕获那些没有写 .catch() 的异步调用。

资源加载错误: 捕获图片、CSS、JS 文件的 404 错误。

Vue 错误: 提供专门的 Vue.config.errorHandler 适配器,因为框架内部的错误有时不会冒泡到 window

ajax错误:劫持 XMLHttpRequest 和 fetch,获取状态码获取错误请求

上报时机:

当捕获到错误时push到任务队列中

定量触发:

如果当前 Task 队列中的任务数量超过了阈值(默认通常是 20 条),它会无视一分钟定时器,直接发起 HTTP 请求。

当页面发生“错误风暴”(比如循环报错)时,能迅速清空内存,防止浏览器卡死

定时触发:

setInterval(() => { Task.fireTasks(); }, 60000);

用户访问页面只产生了一个微小的资源加载错误,没达到定量阈值,一分钟的延迟保证了数据的相对实时性,又兼顾了性能

卸载触发:

上报逻辑采用 navigator.sendBeacon,普通的ajax 由于页面即将销毁,浏览器会为了节省资源直接**截断(Abort)**这个请求,导致数据发不出去,而navigator.sendBeacon 把数据交给浏览器内核,即便页面关了,内核也会确保在后台发完

if (typeof navigator.sendBeacon === 'function') {
      navigator.sendBeacon(
        this.url,
        new Blob([JSON.stringify(data)], {
          type: 'application/json'
        })
      );
      return;
}

浏览器性能模块:

监控页面的流畅度,量化用户感知到的加载速度

监控内容: 传统的 Navigation Timing(如 DNS、TCP、白屏时间)

网络与连接阶段(底层协议耗时)

  • dnsTime (域名解析): domainLookupEnd - domainLookupStart。含义: 浏览器把域名(如 blackztt.cn)转换成 IP 地址的时间。如果这个值很高,可能需要考虑 CDN 预解析(dns-prefetch)。
  • tcpTime (TCP 建立连接): connectEnd - connectStart。含义: 三次握手的时间。
  • sslTime (SSL 安全握手): connectEnd - secureConnectionStart。含义: 仅在 HTTPS 下有效。如果握手慢,说明服务器的 SSL 证书配置或协议版本(如 TLS 1.2 vs 1.3)有优化空间。
  • ttfbTime (首字节时间): responseStart - requestStart。含义: 核心指标。发送请求到收到第一个字节的时间。它直接反映了后端接口的响应速度和网络链路延迟。

传输与渲染准备阶段

  • transTime (内容传输): responseEnd - responseStart。含义: 报文从服务器下载到浏览器的时间。值大通常说明 HTML 报文太大(未开启 Gzip)或带宽受限。
  • fptTime (白屏时间): responseEnd - fetchStart。含义: 用户看到屏幕开始有反应的时间。在 SkyWalking 的这个逻辑里,它近似等于从开始请求到 HTML 下载完成。
  • domAnalysisTime (解析 DOM): domInteractive - responseEnd。含义: 浏览器解析 HTML 结构生成 DOM 树的耗时。

用户感知与交互阶段

  • domReadyTime (用户可操作): domContentLoadedEventEnd - fetchStart。含义: 对应 DOMContentLoaded 事件。此时脚本已加载执行完,用户可以点击按钮了。
  • ttlTime (交互准备时间): domInteractive - fetchStart。含义: 页面达到“可交互”状态的时间。
  • loadPageTime (整页加载完成): loadEventStart - fetchStart。含义: 对应 window.onload。此时页面上所有的图片、框架等外部资源全部加载完毕。
  • resTime (同步资源加载): loadEventStart - domContentLoadedEventEnd。含义: 专门衡量 HTML 解析完之后,那些同步加载的图片/资源拖了多久的后腿。

上报时机:

页面加载或离开时上报,同样页面离开时上报采用navigator.sendBeacon,并保底降级xhr请求

网络请求模块:

在 register 阶段通过原型链劫持,接管浏览器所有的通信出口

劫持 XMLHttpRequest (XHR)

探针会保存原生的 XMLHttpRequest,然后用一个包装类替换它:

  • 拦截 open():获取请求的方法(GET/POST)和 URL。
  • 拦截 send():在请求真正发出前,向 Request Header 注入 sw8 字段。
  • 监听 onreadystatechange:当请求完成时,记录结束时间、状态码,并判断是否需要触发 AJAX_ERROR。

sw8请求头包含哪些内容 ${1}-${traceIdStr}-${segmentId}-${index}-${service}-${instance}-${endpoint}-${peer}

序号字段变量名含义解析
11 (固定值)Sample (采样标志)1 代表该请求需要被采集并上报。如果设为 0,后端 OAP 收到后将丢弃该链路,不存储。
2traceIdStrTrace ID全链路唯一标识。从前端点击到后端微服务 A -> B -> C,这个 ID 永远不变。它是串联整个调用链的“主键”。
3segmentIdParent Segment ID前端段 ID。在 SkyWalking 中,一个服务内的一组操作叫 Segment。这里代表发起请求的“前端这个动作”的 ID。
4indexSpan ID跨度索引。代表这是当前 Segment 下的第几个操作(通常从 0 开始)。后端根据它来确定请求的先后顺序。
5serviceParent Service应用名称。在 register 时配置的 service。后端拓扑图中显示“来源”是谁,全靠它。
6instanceParent Service Instance服务实例。前端通常是浏览器指纹或随机 ID。用于区分是哪个具体用户或设备发出的请求。
7endpointParent Endpoint入口路径。记录当前报错或请求发生时,用户正处于哪个页面(pagePath)。
8peerPeer Address目标地址。即接口所在的 Host。后端用来识别这个请求是发往哪个目标集群的。

劫持Fetch

保存引用: 首先把浏览器原生的 window.fetch 保存到一个私有变量中(如 originFetch)。

重写函数: 给 window.fetch 重新赋值一个自定义函数。

注入逻辑: 在这个自定义函数内部:生成 traceId 和 sw8 请求头。将 sw8 塞入请求的 headers 配置中。记录开始时间。执行原生引用: 调用之前保存的 originFetch 真正发出请求。处理响应: 监听 Promise 的 then 和 catch,计算耗时,记录状态码,最后将数据入队。

上报时机:

请求结束时(xhr.onload 或 fetch.then),拿到响应时间、状态码后,才能生成完整的 Segment 数据入队

定时上报:

setInterval(() => {
    if (!segments.length) {
      return;
    }
    new Report('SEGMENTS', options.collector).sendByXhr(segments);
    segments.splice(0, segments.length);
}, options.traceTimeInterval);

卸载触发:

同样采用 navigator.sendBeacon 进行上报

assistant