十分钟快速实现Web应用性能监控

前端性能监控在优化用户体验和提升业务指标方面发挥着关键作用(主要是可以拿着性能数据对外吹牛B)。有需求就有产品,目前主流云厂商基本提供了类似的SaaS产品,比如阿里云的ARMS 用户体验监控,腾讯云的前端性能监控 RUM

这些SaaS产品提供了非常丰富的性能指标,然而这些指标对于小规模应用真的有必要吗?项目真的有预算去买这些服务?答案是否定的,在项目规模没起来前,谁管那么细的性能指标啊,能关注个首屏时间就很不错了。

好的,作为开发者我们肯定想了解一下自己web应用的性能指标,怎么做呢?请看下文,包你10分钟快速实现主要性能指标的采集与上报。

核心指标

关于性能指标有很多,目前社区必要认可的监控指标体系是 GoogleChrome 提出的web-vitals关键指标集合:

  1. CLS:整个页面生命周期内发生的所有意外布局偏移中最大一连串的布局偏移分数
  2. FID:用户第一次和页面交互后(比如点击按钮),浏览器响应并处理事件的时间
  3. LCP:最大内容绘制的时间,即视口内最大元素渲染的时间点
  4. FCP:首次内容绘制,开始加载到页面内容的任何部分在屏幕上完成。
  5. TTFB:页面返回首字节时间,用来判断页面资源返回速度

备注:在最新的 web-vital指标体系里 FID 已经被 INP 所取代,并且官方明确在下一个 release版本中将移除 FID指标

考虑到认可度,本文还是采纳老的指标体系

性能指标采集

明确了性能指标,那就开工吧。

首先要明白对于性能指标,最可靠,最稳妥的获取方式一定是通过浏览器提供的"原生" BOM 接口实现,带着这个假设去 MDN 上搜索相关草案,还真的就找到了一个 API

PerformanceObserver()

PerformanceObserver() 构造函数使用给定的观察者 callback 生成一个新的 PerformanceObserver 对象。当通过 observe() 方法注册的 条目类型性能条目事件 被记录下来时,调用该观察者回调。

语法

var observer = new PerformanceObserver(callback);

通过提问 GPT,可以得知 PerformanceObserver 源自 2022 年 11 月 15 日发布了候选推荐草案里的Performance Timeline Level 2 标准。

额~,2022年提出的草案,也就说是 2022 年之前版本的浏览器就不要想着能完全使用了。不过没关系,我们还有兜底方案,首先根据 MDN 指引文档来借助 PerformanceObserver 实现上述 5 个核心指标的采集

PerformanceObserver

此处略过为什么,因为我也不知道,按照 MDN 文档提供的API使用就是了

具体实现如下:

js 复制代码
// CLS
const clsObserver = new PerformanceObserver(entryList => {
  const clsEntries = entryList.getEntries()
  const clsValue = clsEntries.reduce((sum, entry: any) => sum + entry.value, 0)
  console.log('CLS:', clsValue)
})

clsObserver.observe({ type: 'layout-shift', buffered: true })

// FID
const fidObserver = new PerformanceObserver(entryList => {
  const fidEntry: any = entryList.getEntries()[0]
  const fidValue = fidEntry.processingStart - fidEntry.startTime
  console.log('FID:', fidValue)
})

fidObserver.observe({ type: 'first-input', buffered: true })

// LCP
const lcpObserver = new PerformanceObserver(entryList => {
  const lcpEntry = entryList.getEntries()[0]
  const lcpValue = lcpEntry.startTime
  console.log('LCP:', lcpValue)
})

lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true })

// FCP
const fcpObserver = new PerformanceObserver(entryList => {
  const fcpEntry = entryList.getEntries()[0]
  const fcpValue = fcpEntry.startTime
  console.log('FCP:', fcpValue)
})

fcpObserver.observe({ type: 'paint', buffered: true })

// TTFB
const ttfbObserver = new PerformanceObserver(entryList => {
  const clsEntries = entryList.getEntries()
  const clsValue = clsEntries.reduce((sum, entry: any) => sum + entry.value, 0)
  console.log('TTFB:', clsValue)
})

ttfbObserver.observe({ type: 'navigation', buffered: true })

唉,上面的代码怎么只有采集没有上报?...数据都采集到了,写个 Restful 接口实现上报应该不难了吧^_^。

好的,从上面代码可以看到采集的关键是 observe 方法,而具体是哪个指标则是由 type: xxx 来决定的。刚才我们也提到了兼容性问题,那么 PerformanceObserver 的兼容性如何呢?

可以看到 PerformanceObserver 本身的兼容性还是不错的,但是,其对指标采集起到重要作用的 type兼容性就呵呵了,以 LCP 为例:

Safari下完全不支持,在国产浏览器上的支持度不明。

那怎么办?既然GoogleChrome提出了这些核心指标集,ta肯定也提供了上报手段,那就是web-vitals

The web-vitals library is a tiny (~2K, brotli'd), modular library for measuring all the Web Vitals metrics on real users, in a way that accurately matches how they're measured by Chrome and reported to other Google tools (e.g. Chrome User Experience Report, Page Speed Insights, Search Console's Speed Report).

The library supports all of the Core Web Vitals as well as a number of other metrics that are useful in diagnosing real-user performance issues.

所以我们可以用 web-vitals 这个库来做兜底,当浏览器不支持PerformanceObserver相应的 type 时,调用web-vitals暴露的方法实现性能指标采集。

web-vitals

web-vitals的使用非常简单:

js 复制代码
import { onCLS, onFID, onFCP, onLCP, onTTFB } from "web-vitals";

const reportWebVitals = () => {
  onCLS((metric) => {
    console.log("CLS = ", metric);
  });
  onFCP((metric) => {
    console.log("FCP = ", metric);
  });
  onFID((metric) => {
    console.log("FID = ", metric);
  });
  onLCP((metric) => {
    console.log("LCP = ", metric);
  });
  onTTFB((metric) => {
    console.log("TTFB = ", metric);
  });
};

export default reportWebVitals;

是不是很简单,直接在暴露出的相关方法的回调里接收数值就好了。

最终版

综合上面的措施,在PerformanceObserver 监听的基础上加一点兼容代码,就可以得到相对完善的性能监控方法:

js 复制代码
import { onLCP, onFID, onCLS, onFCP, onTTFB } from 'web-vitals'

// CLS
function observeCLS() {
  if (this.supportedEntryTypes.includes('layout-shift')) {
    const clsObserver = new PerformanceObserver(entryList => {
      const clsEntries = entryList.getEntries()
      const clsValue = clsEntries.reduce((sum, entry: any) => sum + entry.value, 0)
      console.log('CLS:', clsValue)
    })

    clsObserver.observe({ type: 'layout-shift', buffered: true })
  } else {
    onCLS(res => {
      this.WebPerformanceTracer.startActiveSpan('cls', clsSpan => {
        clsSpan.setAttributes({ cls: res.value, path: location.pathname })
        clsSpan.end()
      })
    })
  }
}

// FID
function observeFID() {
  if (this.supportedEntryTypes.includes('first-input')) {
    const fidObserver = new PerformanceObserver(entryList => {
      const fidEntry: any = entryList.getEntries()[0]
      const fidValue = fidEntry.processingStart - fidEntry.startTime
      console.log('FID:', fidValue)
    })

    fidObserver.observe({ type: 'first-input', buffered: true })
  } else {
    onFID(res => {
      console.log('FID:', res.value)
    })
  }
}

// LCP
function observeLCP() {
  if (this.supportedEntryTypes.includes('largest-contentful-paint')) {
    const lcpObserver = new PerformanceObserver(entryList => {
      const lcpEntry = entryList.getEntries()[0]
      const lcpValue = lcpEntry.startTime
      console.log('LCP:', lcpValue)
    })

    lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true })
  } else {
    onLCP(res => {
      console.log('LCP:', res.value)
    })
  }
}

// FCP
function observeFCP() {
  if (this.supportedEntryTypes.includes('paint')) {
    const fcpObserver = new PerformanceObserver(entryList => {
      const fcpEntry = entryList.getEntries()[0]
      const fcpValue = fcpEntry.startTime
      console.log('FCP:', fcpValue)
    })

    fcpObserver.observe({ type: 'paint', buffered: true })
  } else {
    onFCP(res => {
      console.log('FCP:', res.value)
    })
  }
}

// TTFB
function observeTTFB() {
  if (this.supportedEntryTypes.includes('navigation')) {
    const ttfbObserver = new PerformanceObserver(entryList => {
      const clsEntries = entryList.getEntries()
      const clsValue = clsEntries.reduce((sum, entry: any) => sum + entry.value, 0)
      console.log('TTFB:', clsValue)
    })

    ttfbObserver.observe({ type: 'navigation', buffered: true })
  } else {
    onTTFB(res => {
      console.log('TTFB:', res.value)
    })
  }
}

怎么样?是不是很简单就实现了web站点的核心性能指标采集。

总结

  1. 一般来讲在项目没有起规模前,只需要关注web应用的核心指标就行;
  2. 通过PerformanceObserver可以实现初步的性能指标采集能力;
  3. 借助 web-vitals 库可以进一步提供性能指标采集的适用性、兼容性。
相关推荐
忘不了情13 分钟前
react中,使用antd的Upload组件上传zip压缩包文件
前端·javascript·react.js
冰淇淋百宝箱30 分钟前
GraphRAG: Auto Prompt Tuning 实践
java·服务器·前端
记得开心一点嘛1 小时前
uniapp --- 配置文件
前端·typescript·uni-app
Bingo_BIG1 小时前
uni-app vue3 常用页面 组合式api方式
前端·javascript·uni-app
无限大.1 小时前
基于 HTML5 Canvas 制作一个精美的 2048 小游戏--day2
前端·html·html5
索然无味io2 小时前
PHP基础--流程控制
前端·笔记·后端·学习·web安全·网络安全·php
新生派2 小时前
HTML<img>标签
前端·html
嘿siri2 小时前
html全局遮罩,通过websocket来实现实时发布公告
前端·vue.js·websocket·前端框架·vue·html
Lorcian2 小时前
web前端1--基础
前端·python·html5·visual studio code
不爱学英文的码字机器3 小时前
[JavaScript] 深入理解流程控制结构
开发语言·前端·javascript