前端埋点指南

698 阅读9分钟

前端埋点

1. 埋点定义

埋点是数据采集领域(尤其是用户行为数据采集领域)的术语,指的是针对特定用户行为或事件进行捕获、处理和发送的相关技术及其实施过程。

如用户某个icon点击次数、观看某个视频的时长等。埋点的技术实质,是先监听软件应用运行过程中的事件,当需要关注的事件发生时进行判断和捕获。

image.png

2. 埋点的重要性

数据从产生到应用于产品优化整个流程大概如下所示:数据生产➡️数据采集➡️数据处理➡️数据分析➡️数据驱动➡️产品优化或迭代,埋点是整个流程的开始点,产品优化是整个流程的终点。

3. 埋点类型

埋点主要分为3类:展现埋点 + 曝光埋点 + 交互埋点

  1. 展现埋点

服务端被触发后,用户侧将会展现什么内容。展现埋点需要记录页面展现的内容信息,即服务端下发的内容是什么(不包含一些交互信息)

  1. 曝光埋点

屏幕有限,但内容可以无限,哪些内容被用户侧实际看到(曝光),需要记录的是单个“内容”被看到,一系列被下发的内容,可以触发多次曝光埋点

  1. 交互埋点

交互埋点表明的是功能或内容被用户“点击”了。从埋点时机来说,这个是展现和曝光的下游,记录用户对可交互功能或信息的“消费”情况

🎯 埋点的三种主流方式

1. 代码埋点(手动挡)

在需要监听的位置手动插入追踪代码

// 按钮点击埋点
const handleClick = () => {
  track('button_click', { 
    page: 'home',
    button_id: 'submit_btn' 
  })
}

将信息以属性的形式添加到DOM元素上,统一在body上面挂载事件,利用冒泡机制实现数据捕获和上报

<span 
  data-tpm='vpxRlRxO8f1LAYjWc9jNOcGpIj5Fx6N0'
  data-tpm-args='{"pid":1, "uid":2}'
>
  登录
</span>
document.body.addEventListener('click', function(e) {
  const el = e.target;
  // 获取数据
  const dataTpm = getNodeAttr(el,'data-tpm'); 
  const dataTpmArgs = getNodeAttr(el,'data-tpm-args'); 
  if (dataTpm || dataTpmArgs) {
    // 上报到服务器
    const data = { type: 'click',ec: dataTpm,ea: dataTpmArgs};
    sendToWeb(data);
  }
}, false);

✅ 精准控制,数据丰富 ❌ 侵入性强,维护成本高

2. 可视化埋点(自动挡)

通过圈选工具配置埋点,如GrowingIO、神策

✅ 无需发版,运营可自助 ❌ 只能采集UI层级,无法获取深层业务数据

3. 无埋点(全自动)

全量采集所有用户行为

// 改写原型链实现自动采集
const originalClick = HTMLElement.prototype.click;
HTMLElement.prototype.click = function() {
  track('element_click', {
    tagName: this.tagName,
    innerText: this.innerText.slice(0, 20)
  });
  return originalClick.apply(this, arguments);
}

✅ 无侵入,覆盖全面 ❌ 数据量大,传输和存储成本高

💾 数据存储

1. 内存存储(最常见)

class MemoryStorage {
  queue = []  // 直接存储在内存
  
  // 控制内存占用
  MAX_MEMORY_SIZE = 1000
  
  add(item) {
    if (this.queue.length >= this.MAX_MEMORY_SIZE) {
      // 策略1:丢弃最老数据
      this.queue.shift()
      
      // 策略2:强制发送(但可能失败)
      // this.forceFlush()
    }
    this.queue.push(item)
  }
}

性能影响

  • 普通场景下完全OK(1000条埋点 ≈ 100KB)
  • 极端情况(快速滚动、疯狂点击)可能内存飙升
  • 解决方案:设置上限 + 降级策略

2. localStorage - 持久化存储

class LocalStorageStore {
  STORAGE_KEY = 'tracker_queue'
  MAX_SIZE = 5 * 1024 * 1024  // 5MB限制
  
  add(event) {
    const queue = this.getQueue()
    queue.push(event)
    
    // 估算大小,超限则丢弃
    const size = new Blob([JSON.stringify(queue)]).size
    if (size > this.MAX_SIZE) {
      queue.shift()  // 丢弃最早的一条
    }
    
    localStorage.setItem(this.STORAGE_KEY, JSON.stringify(queue))
  }
  
  getQueue() {
    try {
      return JSON.parse(localStorage.getItem(this.STORAGE_KEY)) || []
    } catch {
      return []
    }
  }
}

适用场景

  • 需要断网续传
  • 页面关闭后不丢数据
  • 跨页面共享埋点队列

⚠️ 注意:同步操作可能阻塞渲染,建议用 setTimeout 包装

3. IndexedDB - 大型存储

class IndexedDBStore {
  db = null
  
  async init() {
    this.db = await new Promise((resolve, reject) => {
      const request = indexedDB.open('TrackerDB', 1)
      request.onupgradeneeded = (e) => {
        const db = e.target.result
        if (!db.objectStoreNames.contains('events')) {
          // 创建对象仓库,使用时间戳作为索引
          const store = db.createObjectStore('events', { 
            keyPath: 'id', 
            autoIncrement: true 
          })
          store.createIndex('timestamp', 'timestamp')
        }
      }
      request.onsuccess = () => resolve(request.result)
    })
  }
  
  async add(event) {
    const tx = this.db.transaction(['events'], 'readwrite')
    const store = tx.objectStore('events')
    await store.add({
      ...event,
      timestamp: Date.now(),
      status: 'pending'
    })
  }
  
  async getBatch(limit = 20) {
    const tx = this.db.transaction(['events'], 'readonly')
    const store = tx.objectStore('events')
    const index = store.index('timestamp')
    
    // 获取最早的一批待发送数据
    return await index.getAll(IDBKeyRange.lowerBound(0), limit)
  }
}

✅ 优点:存储空间大(250MB+),支持复杂查询
❌ 缺点:API复杂,异步操作,学习成本高

💎建议

  1. 中小型应用:内存存储 + requestIdleCallback
  2. 需要断网续传:IndexedDB + 定时器兜底
  3. 超高并发:Web Worker + 内存队列
  4. 既要又要:混合存储方案

💡 批量上报方案

1. 定时器方案(最常用)

class BatchTracker {
  queue = []
  
  constructor(config) {
    this.maxSize = config.maxSize || 10  // 条数阈值
    this.timeWindow = config.timeWindow || 2000  // 时间阈值
    this.initTimer()
  }
  
  initTimer() {
    setInterval(() => {
      if (this.queue.length > 0) {
        this.flush()
      }
    }, this.timeWindow)
  }
  
  track(event) {
    this.queue.push(event)
    
    // 条数达到阈值立即发送
    if (this.queue.length >= this.maxSize) {
      this.flush()
    }
  }
}

优点:实现简单,控制精准
缺点:一直占用定时器,即使没有数据也在跑

2. requestIdleCallback - 浏览器空闲时发送

class IdleTracker {
  queue = []
  isScheduled = false
  
  track(event) {
    this.queue.push(event)
    this.scheduleIdleFlush()
  }
  
  scheduleIdleFlush() {
    if (this.isScheduled) return
    
    this.isScheduled = true
    requestIdleCallback(
      (deadline) => {
        // 在空闲时间发送,但不超过50ms
        while (this.queue.length > 0 && deadline.timeRemaining() > 5) {
          this.sendBatch(this.queue.splice(0, 5))
        }
        
        this.isScheduled = false
        // 如果还有剩余,继续调度
        if (this.queue.length > 0) {
          this.scheduleIdleFlush()
        }
      },
      { timeout: 2000 } // 最多等2秒
    )
  }
}

✅ 优点:不抢占主线程,对性能影响最小
❌ 缺点:兼容性问题(Safari不支持),发送时机不可控

3. IntersectionObserver - 利用元素曝光触发

元素出现在可视区域时上报数据

class ObserverTracker {
  queue = []
  observer = null
  
  constructor() {
    // 创建一个隐藏的发送触发器
    const trigger = document.createElement('div')
    trigger.style.height = '1px'
    trigger.style.opacity = '0'
    document.body.appendChild(trigger)
    
    this.observer = new IntersectionObserver(
      (entries) => {
        if (entries[0].isIntersecting && this.queue.length > 0) {
          // console.log('元素出现啦!')
          // 只触发一次!
          this.flush()
        }
      },
      { threshold: 0.1 }
    )
    
    this.observer.observe(trigger)
  }
  
  track(event) {
    this.queue.push(event)
  }
}

✅ 优点:利用浏览器原生机制,不额外占用资源
❌ 缺点:依赖DOM,发送时机被动

4. Web Worker - 独立线程处理

// tracker-worker.js
self.addEventListener('message', (e) => {
  const { type, data } = e.data
  const queue = []
  
  if (type === 'track') {
    queue.push(data)
    
    // 在worker里做批量处理
    if (queue.length >= 10) {
      self.postMessage({ type: 'flush', data: [...queue] })
      queue.length = 0
    }
  }
})

// main.js
const worker = new Worker('tracker-worker.js')
const tracker = {
  track(event) {
    worker.postMessage({ type: 'track', data: event })
  }
}

worker.addEventListener('message', (e) => {
  if (e.data.type === 'flush') {
    // 主线程发送数据
    navigator.sendBeacon('/api/track', JSON.stringify(e.data.data))
  }
})

✅ 优点:完全不阻塞主线程
❌ 缺点:通信开销,实现复杂

🚀 高级埋点实践技巧

1. 批量上报与防抖

class Tracker {
  queue = []
  timer = null
  
  track(event) {
    this.queue.push(event)
    this.schedule()
  }
  
  schedule() {
    if (this.timer) return
    this.timer = setTimeout(() => {
      this.flush()
      this.timer = null
    }, 2000)
  }
  
  async flush() {
    if (this.queue.length === 0) return
    const events = [...this.queue]
    this.queue = []
    
    try {
      await navigator.sendBeacon('/api/track', JSON.stringify(events))
    } catch {
      // 降级方案
      this.queue.unshift(...events)
    }
  }
}

2. 用户行为串联

// 生成唯一session ID
const sessionId = uuidv4()

// 维护用户行为栈
const behaviorStack = []
const MAX_STACK_SIZE = 50

function pushBehavior(action) {
  behaviorStack.push({
    ...action,
    timestamp: Date.now(),
    sessionId
  })
  
  if (behaviorStack.length > MAX_STACK_SIZE) {
    behaviorStack.shift()
  }
}

3. 隐私数据脱敏

function sanitizeData(data) {
  const sensitiveKeys = ['phone', 'email', 'idCard']
  
  return Object.keys(data).reduce((acc, key) => {
    if (sensitiveKeys.includes(key)) {
      acc[key] = '***'
    } else {
      acc[key] = data[key]
    }
    return acc
  }, {})
}

⚠️ 埋点常见问题与解决方案

1. 数据重复上报

  • 原因:组件重复渲染、路由切换未清理
  • 解决:使用WeakMap缓存已上报元素
const reportedElements = new WeakMap()

function trackIfNotReported(element, event) {
  if (reportedElements.has(element)) return
  reportedElements.set(element, true)
  track(event)
}

2. 异步加载场景丢失

// 监听动态内容加载
const observer = new MutationObserver((mutations) => {
  mutations.forEach(mutation => {
    mutation.addedNodes.forEach(node => {
      if (node.nodeType === 1) {
        initTrackingOnElement(node)
      }
    })
  })
})

observer.observe(document.body, {
  childList: true,
  subtree: true
})

3. 页面卸载数据丢失

window.addEventListener('beforeunload', () => {
  // 同步发送剩余数据
  if (tracker.queue.length > 0) {
    navigator.sendBeacon('/api/track', JSON.stringify(tracker.queue))
  }
})

⚡️ 前端性能指标采集

📊 核心性能指标全景图

时间轴 →
┌─────────┬─────────┬──────────┬──────────┬──────────┐
│   FP    │   FCP   │   LCP    │   FID    │   TTI    │
├─────────┼─────────┼──────────┼──────────┼──────────┤
│ 白屏时间 │ 内容绘制  │ 最大内容  │ 交互延迟  │ 可交互时间 │
└─────────┴─────────┴──────────┴──────────┴──────────┘

1. Paint Timing API - 绘制指标

class PaintMetricsCollector {
  metrics = {
    fp: null,  // First Paint
    fcp: null, // First Contentful Paint
    lcp: null  // Largest Contentful Paint (通过PerformanceObserver单独获取)
  }
  
  constructor() {
    this.initPaintTiming()
    this.initLCP()
  }
  
  initPaintTiming() {
    // 获取首次绘制时间
    const paintEntries = performance.getEntriesByType('paint')
    paintEntries.forEach(entry => {
      if (entry.name === 'first-paint') {
        this.metrics.fp = entry.startTime
      }
      if (entry.name === 'first-contentful-paint') {
        this.metrics.fcp = entry.startTime
      }
    })
    
    // 或者用Observer监听(更实时)
    const paintObserver = new PerformanceObserver((list) => {
      list.getEntries().forEach(entry => {
        console.log(`🎨 ${entry.name}: ${entry.startTime}ms`)
        this.metrics[entry.name === 'first-paint' ? 'fp' : 'fcp'] = entry.startTime
      })
    })
    paintObserver.observe({ entryTypes: ['paint'] })
  }
  
  initLCP() {
    // LCP需要持续监听,因为最大元素可能变化
    const lcpObserver = new PerformanceObserver((list) => {
      const entries = list.getEntries()
      const lastEntry = entries[entries.length - 1]
      this.metrics.lcp = lastEntry.startTime
      console.log(`🖼️ LCP: ${lastEntry.startTime}ms`, lastEntry.element)
    })
    lcpObserver.observe({ entryTypes: ['largest-contentful-paint'] })
  }
}

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

class FIDCollector {
  fid = null
  
  constructor() {
    this.initFID()
  }
  
  initFID() {
    // 方案1:使用PerformanceObserver
    if (PerformanceObserver.supportedEntryTypes?.includes('first-input')) {
      const fidObserver = new PerformanceObserver((list) => {
        list.getEntries().forEach(entry => {
          // FID = 处理延迟时间(processingStart) - 时间戳(startTime)
          this.fid = entry.processingStart - entry.startTime
          console.log(`👆 FID: ${this.fid}ms`)
          
          // 上报数据
          this.reportFID({
            fid: this.fid,
            target: entry.target?.tagName,
            type: entry.name
          })
        })
      })
      fidObserver.observe({ entryTypes: ['first-input'] })
    }
    
    // 方案2:手动监听(兼容性更好)
    this.manualFID()
  }
  
  manualFID() {
    let firstInputTime = null
    
    const handleFirstInput = (event) => {
      if (firstInputTime) return
      
      firstInputTime = Date.now()
      const inputDelay = event.timeStamp // 事件发生时间
      
      // 计算从事件发生到开始处理的时间差
      setTimeout(() => {
        const processingStart = Date.now()
        const fid = processingStart - firstInputTime
        
        console.log(`👆 [手动] FID: ${fid}ms`)
      }, 0)
      
      // 移除监听
      ['click', 'touchstart', 'keydown'].forEach(type => {
        window.removeEventListener(type, handleFirstInput, true)
      })
    }
    
    // 在捕获阶段监听,确保最早获取
    ['click', 'touchstart', 'keydown'].forEach(type => {
      window.addEventListener(type, handleFirstInput, true)
    })
  }
}

3. TTI (Time to Interactive) - 可交互时间

class TTICollector {
  tti = null
  
  async calculateTTI() {
    // 获取关键时间点
    const fcp = this.getFCP()
    const domInteractive = performance.timing.domInteractive
    
    // 方案:寻找5秒内没有长任务的时间窗口
    const longTasks = await this.getLongTasks()
    
    let tti = Math.max(fcp, domInteractive)
    
    // 从FCP之后找第一个安静窗口
    for (let i = 0; i < longTasks.length; i++) {
      const task = longTasks[i]
      if (task.startTime < tti) continue
      
      // 检查这个长任务之后是否有5秒安静期
      const nextTask = longTasks[i + 1]
      if (!nextTask || nextTask.startTime - task.startTime > 5000) {
        tti = task.startTime + task.duration
        break
      }
    }
    
    this.tti = tti
    console.log(`⏱️ TTI: ${tti}ms`)
    return tti
  }
  
  getLongTasks() {
    return new Promise(resolve => {
      const tasks = []
      
      if (PerformanceObserver.supportedEntryTypes?.includes('longtask')) {
        const observer = new PerformanceObserver((list) => {
          list.getEntries().forEach(entry => {
            tasks.push({
              startTime: entry.startTime,
              duration: entry.duration,
              name: entry.name
            })
          })
        })
        
        observer.observe({ entryTypes: ['longtask'] })
        
        // 页面加载完成后停止收集
        setTimeout(() => {
          observer.disconnect()
          resolve(tasks)
        }, 10000)
      } else {
        resolve([])
      }
    })
  }
}

4. 资源加载监控

class ResourceMetricsCollector {
  resources = []
  
  constructor() {
    this.initResourceObserver()
    this.calculateCriticalResources()
  }
  
  initResourceObserver() {
    const resourceObserver = new PerformanceObserver((list) => {
      list.getEntries().forEach(entry => {
        // 过滤掉跨域资源(如果没有Timing-Allow-Origin头)
        if (!entry.transferSize) return
        
        this.resources.push({
          name: entry.name,
          type: entry.initiatorType,
          duration: entry.duration,
          size: entry.transferSize,
          protocol: entry.nextHopProtocol,
          dns: entry.domainLookupEnd - entry.domainLookupStart,
          tcp: entry.connectEnd - entry.connectStart,
          ttfb: entry.responseStart - entry.requestStart,
          download: entry.responseEnd - entry.responseStart
        })
      })
    })
    
    resourceObserver.observe({ entryTypes: ['resource'] })
  }
  
  // 计算关键资源加载时间
  calculateCriticalResources() {
    window.addEventListener('load', () => {
      const criticalResources = this.resources.filter(r => {
        // 首屏关键资源判断
        return r.type === 'script' || 
               r.type === 'link' || 
               (r.type === 'img' && this.isAboveTheFold(r.name))
      })
      
      console.log('📦 关键资源统计:', {
        count: criticalResources.length,
        totalSize: criticalResources.reduce((sum, r) => sum + r.size, 0),
        maxLoadTime: Math.max(...criticalResources.map(r => r.duration))
      })
    })
  }
}

5. CLS (Cumulative Layout Shift) - 布局偏移

class CLSCollector {
  clsValue = 0
  sessionWindows = []
  
  constructor() {
    this.initCLSObserver()
  }
  
  initCLSObserver() {
    let cls = 0
    let sessionWindow = {
      start: performance.now(),
      entries: []
    }
    
    const clsObserver = new PerformanceObserver((list) => {
      list.getEntries().forEach(entry => {
        if (!entry.hadRecentInput) { // 忽略500ms内的输入
          const currentTime = performance.now()
          
          // 检查是否应该开始新的会话窗口
          if (currentTime - sessionWindow.start > 5000) {
            // 记录上一个窗口
            this.sessionWindows.push({
              ...sessionWindow,
              value: cls
            })
            
            // 开始新窗口
            sessionWindow = {
              start: currentTime,
              entries: [entry]
            }
            cls = entry.value
          } else {
            // 当前窗口累加
            sessionWindow.entries.push(entry)
            cls += entry.value
          }
          
          this.clsValue = Math.max(this.clsValue, cls)
          console.log('📐 CLS:', this.clsValue)
        }
      })
    })
    
    clsObserver.observe({ entryTypes: ['layout-shift'] })
  }
}

🚀 综合性能监控系统

class PerformanceMonitor {
  constructor(reportCallback) {
    this.report = reportCallback
    this.metrics = {}
    
    this.initAllObservers()
    this.calculateCustomMetrics()
  }
  
  initAllObservers() {
    // 绘制指标
    this.observePaint()
    
    // 加载指标
    this.observeNavigation()
    
    // 资源指标
    this.observeResources()
    
    // 交互指标
    this.observeInteractions()
    
    // 布局指标
    this.observeLayout()
  }
  
  observeNavigation() {
    const navObserver = new PerformanceObserver((list) => {
      const [entry] = list.getEntries()
      this.metrics.navigation = {
        dns: entry.domainLookupEnd - entry.domainLookupStart,
        tcp: entry.connectEnd - entry.connectStart,
        ssl: entry.secureConnectionStart ? 
             entry.connectEnd - entry.secureConnectionStart : 0,
        ttfb: entry.responseStart - entry.requestStart,
        domLoad: entry.domContentLoadedEventEnd - entry.domContentLoadedEventStart,
        load: entry.loadEventEnd - entry.loadEventStart,
        type: entry.type
      }
    })
    
    navObserver.observe({ entryTypes: ['navigation'] })
  }
  
  observeInteractions() {
    // 记录所有交互延迟
    const interactionObserver = new PerformanceObserver((list) => {
      list.getEntries().forEach(entry => {
        const delay = entry.duration // 事件处理耗时
        const target = entry.target?.tagName
        
        console.log(`🎯 交互延迟 [${target}]: ${delay}ms`)
        
        // 上报慢交互(大于100ms)
        if (delay > 100) {
          this.report({
            type: 'slow-interaction',
            data: {
              delay,
              target,
              event: entry.name,
              timestamp: Date.now()
            }
          })
        }
      })
    })
    
    interactionObserver.observe({ 
      entryTypes: ['event'] 
    })
  }
  
  observeLayout() {
    // 检测强制同步布局
    let layoutCount = 0
    
    const layoutObserver = new PerformanceObserver((list) => {
      list.getEntries().forEach(entry => {
        layoutCount++
        
        console.warn('⚠️ 强制同步布局:', {
          script: entry.name,
          duration: entry.duration,
          stack: this.getStackTrace()
        })
        
        this.report({
          type: 'forced-layout',
          data: {
            duration: entry.duration,
            script: entry.name
          }
        })
      })
    })
    
    layoutObserver.observe({ 
      entryTypes: ['layout-shift'] 
    })
  }
  
  calculateCustomMetrics() {
    // 首屏时间
    this.calculateFirstScreenTime()
    
    // 可交互时间
    this.calculateTimeToInteractive()
    
    // 资源加载完成率
    this.calculateResourceLoadRate()
    
    // 内存使用(如果支持)
    if (performance.memory) {
      setInterval(() => {
        this.metrics.memory = {
          used: performance.memory.usedJSHeapSize,
          total: performance.memory.totalJSHeapSize,
          limit: performance.memory.jsHeapSizeLimit
        }
      }, 30000)
    }
  }
  
  calculateFirstScreenTime() {
    // 首屏时间:视口内元素绘制完成的时间
    const viewportElements = this.getViewportElements()
    let maxTime = 0
    
    viewportElements.forEach(el => {
      const time = this.getElementPaintTime(el)
      maxTime = Math.max(maxTime, time)
    })
    
    this.metrics.firstScreenTime = maxTime
  }
  
  // 上报所有指标
  reportAllMetrics() {
    // 等待所有指标收集完成
    setTimeout(() => {
      this.report({
        type: 'performance-summary',
        data: {
          ...this.metrics,
          url: window.location.href,
          userAgent: navigator.userAgent,
          timestamp: Date.now()
        }
      })
    }, 5000)
  }
}

📈 数据上报策略

class PerformanceReporter {
  constructor() {
    this.queue = []
    this.initialized = false
  }
  
  init() {
    if (this.initialized) return
    this.initialized = true
    
    // 页面隐藏时上报(确保数据不丢失)
    document.addEventListener('visibilitychange', () => {
      if (document.visibilityState === 'hidden') {
        this.flushImmediately()
      }
    })
    
    // 定期上报
    setInterval(() => {
      this.flush()
    }, 10000)
  }
  
  // 普通上报
  report(metric) {
    this.queue.push(metric)
    
    if (this.queue.length >= 10) {
      this.flush()
    }
  }
  
  // 立即上报(重要指标)
  reportImmediately(metric) {
    this.sendBeacon([metric])
  }
  
  flush() {
    if (this.queue.length === 0) return
    
    const metrics = [...this.queue]
    this.queue = []
    
    this.sendBeacon(metrics)
  }
  
  flushImmediately() {
    if (this.queue.length === 0) return
    
    this.sendBeacon(this.queue)
    this.queue = []
  }
  
  sendBeacon(metrics) {
    try {
      navigator.sendBeacon('/api/performance', JSON.stringify({
        metrics,
        timestamp: Date.now()
      }))
    } catch (e) {
      // 降级方案
      fetch('/api/performance', {
        method: 'POST',
        body: JSON.stringify(metrics),
        headers: { 'Content-Type': 'application/json' },
        keepalive: true // 关键:确保页面关闭时也能发送
      }).catch(console.error)
    }
  }
}

监控分级策略

const monitoringStrategy = {
  critical: ['LCP', 'FID', 'CLS', 'JS_ERROR'], // 立即上报
  important: ['FP', 'FCP', 'TTFB'], // 批量上报
  normal: ['resource_load', 'api_latency'], // 抽样上报
  verbose: ['long_task', 'layout_shift'] // 本地存储,按需拉取
}