性能预算与持续监控

概述

性能优化最令人沮丧的事情之一,是花费大量时间精心优化后的性能指标,在下一次产品迭代后很快就会退化。新的功能加入、第三方脚本的增加、图片素材的累积——这些变化都可能蚕食你辛苦获得的性能收益。性能优化不应该是一次性的项目,而应该是持续进行的工程实践。

性能预算(Performance Budget)正是解决这个问题的方法。它为团队的代码量和加载时间设定明确的限制,类似于项目的财务预算。当开发者提交代码变更时,性能预算提供了客观的标准来判断这次变更是否对性能产生了负面影响,是否需要进一步优化。

建立性能预算体系不仅仅是设置几个数字那么简单。它需要团队对性能目标达成共识,建立测量基线,制定不同阶段的递进目标,并将性能检查集成到开发流程的各个环节中。当性能预算被突破时,应该有明确的流程来处理——是调整预算目标,还是立即进行优化?

本节将系统讲解如何制定性能预算,包括指标预算、资源预算和 Lighthouse 评分预算三种主要类型。我们将探讨如何在 CI/CD 流水线中集成性能检查,搭建 RUM(Real User Monitoring)监控系统来追踪真实用户性能,以及如何设置报警机制来及时发现性能回归。

目标

  • 理解性能预算的类型、制定方法和团队协作流程
  • 掌握在 CI/CD 中集成性能检查的方案,包括 bundlesize、size-limit、Lighthouse CI
  • 学会搭建基础的 RUM 监控系统,采集和分析真实用户性能数据
  • 建立性能报警与回归检测机制,确保性能不会随迭代退化

知识体系

1. 性能预算的类型

性能预算可以从多个维度来定义,不同类型的预算用于不同的目的。主要的性能预算类型包括基于指标的预算基于资源的预算基于规则的预算

基于指标的预算

基于 Core Web Vitals 或传统性能指标的预算是最直接的性能目标形式。它们直接映射用户体验,因此最具有业务价值。

指标 预算值 说明
LCP ≤ 2.5s 主内容加载时间,Good 阈值
INP ≤ 200ms 交互响应时间,Good 阈值
CLS ≤ 0.1 视觉稳定性,Good 阈值
FCP ≤ 1.8s 首次内容绘制
TTI ≤ 3.8s 可交互时间

这些阈值来自于 Google 定义的 Core Web Vitals Good 区间。但实际项目中,应该根据自己产品的特点和用户群体的设备分布来调整目标。比如面向低端 Android 用户的应用,可能需要将 TTI 目标设定得更宽松一些。

基于资源的预算

资源预算关注的是页面消耗的带宽和系统资源。这类预算的好处是更容易追踪到具体的问题代码——当某个 bundle 变大时,你可以直接定位是哪个模块增加了体积。

// 资源体积预算
const resourceBudgets = {
  totalPageWeight: 500 * 1024,     // 总页面 500KB(压缩后)
  javascript: 200 * 1024,          // JS 200KB
  css: 50 * 1024,                  // CSS 50KB
  images: 200 * 1024,              // 图片 200KB
  fonts: 100 * 1024,               // 字体 100KB
  thirdParty: 100 * 1024,          // 第三方资源 100KB
};

// 请求数预算
const requestBudgets = {
  totalRequests: 50,
  scripts: 10,
  stylesheets: 3,
  images: 20,
  fonts: 4,
  thirdPartyRequests: 10,
};

资源预算通常以压缩后的大小为准,因为用户实际下载的是压缩后的文件。同时要考虑 gzip 或 brotli 压缩后的比例差异——文本文件压缩率高,图片压缩率低甚至没有压缩。

基于规则的预算

Lighthouse 评分预算定义了应用在各项审计中应该达到的最低分数。

// Lighthouse 分数预算
const scoreBudgets = {
  performance: 90,
  accessibility: 90,
  bestPractices: 90,
  seo: 90,
};

这种预算形式的好处是可以从多个维度来衡量应用的健康度,而不仅仅是性能。但需要注意的是,Lighthouse 分数并非用户体验的直接映射——一个 90 分的页面在某些用户看来可能仍然感觉卡顿。

2. 预算制定方法

性能预算的制定不应该拍脑袋决定,而应该基于数据和竞品分析。

预算制定流程通常遵循以下步骤:首先分析竞品,了解同类产品中最佳体验的量化指标,将自己的目标设定为竞品最佳值的 80% 左右。其次收集基线,使用真实用户监控数据获取当前页面的性能分布情况,了解自己用户的真实体验。第三设定阶段目标,将长期目标分解为短期可实现的小目标,逐步改善。第四按页面类型分级,首页、列表页、详情页等不同类型的页面应该有不同的预算。第五团队共识,让所有开发者都理解和认同预算标准。

// 按页面类型设定预算
const budgetsByPageType = {
  homepage: {
    lcp: 2000,
    cls: 0.05,
    totalJS: 150 * 1024,
    totalCSS: 30 * 1024,
  },
  productList: {
    lcp: 2500,
    cls: 0.1,
    totalJS: 200 * 1024,
    totalCSS: 40 * 1024,
  },
  productDetail: {
    lcp: 2500,
    cls: 0.1,
    totalJS: 250 * 1024,
    totalCSS: 50 * 1024,
  },
  checkout: {
    lcp: 2000,
    cls: 0.05,
    totalJS: 180 * 1024,
    totalCSS: 35 * 1024,
  },
};

不同页面类型的预算差异反映了它们的优先级和使用场景。首页通常是用户的第一印象,需要更严格的性能控制;Checkout 页面的转化率直接影响业务收益,也需要优先保证性能。

3. CI/CD 性能检查

将性能检查集成到 CI/CD 流水线中是实现持续性能治理的关键。只有在每次代码变更时都进行检查,才能及时发现性能回归,而不是在用户投诉后才意识到问题。

Webpack 构建预算

Webpack 提供了开箱即用的性能预算功能,可以在构建超限时直接报错。

// webpack.config.js
module.exports = {
  performance: {
    maxEntrypointSize: 250 * 1024,
    maxAssetSize: 200 * 1024,
    hints: 'error',
    assetFilter: (filename) =>
      filename.endsWith('.js') || filename.endsWith('.css'),
  },
};

hints 设置为 'error' 会在超出预算时让构建失败,这对于强制执行预算是有用的。但要注意的是,这个检查是基于未压缩的大小的,所以实际用户体验通常会好于配置的数字。

bundlesize

bundlesize 是一个专门用于检查 npm 包大小的工具,它可以在 CI 中运行并对 PR 评论体积变化。

// package.json
{
  "scripts": {
    "check-bundle": "bundlesize"
  },
  "bundlesize": [
    { "path": "dist/js/main.*.js", "maxSize": "150 kB" },
    { "path": "dist/js/vendor.*.js", "maxSize": "200 kB" },
    { "path": "dist/css/main.*.css", "maxSize": "30 kB" }
  ]
}

bundlesize 支持 gzip 压缩后的体积比较,这在配置 maxSize 时更有实际意义。

size-limit

size-limit 是另一个体积检查工具,它与 GitHub Actions 集成良好,可以在 PR 中自动评论体积变化。

// package.json
{
  "size-limit": [
    {
      "path": "dist/index.js",
      "limit": "10 kB",
      "import": "{ Button }",
      "ignore": ["react", "react-dom"]
    },
    {
      "path": "dist/index.js",
      "limit": "50 kB"
    }
  ]
}

import 字段允许你检查特定 import 的体积,ignore 字段可以排除某些依赖的体积计算。

# .github/workflows/size.yml
name: Size Check
on: [pull_request]

jobs:
  size:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: andresz1/size-limit-action@v1
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          # 自动在 PR 中评论体积变化

Lighthouse CI 集成

Lighthouse CI 是 Google 官方推出的 Lighthouse 持续集成工具,可以在每次 PR 时自动运行 Lighthouse 审计并检查性能预算。

// lighthouserc.js
module.exports = {
  ci: {
    collect: {
      url: [
        'http://localhost:3000/',
        'http://localhost:3000/products',
      ],
      numberOfRuns: 3,
    },
    assert: {
      preset: 'lighthouse:recommended',
      assertions: {
        'categories:performance': ['error', { minScore: 0.9 }],
        'first-contentful-paint': ['error', { maxNumericValue: 1800 }],
        'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
        'total-blocking-time': ['error', { maxNumericValue: 300 }],
        'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
        'resource-summary:script:size': ['error', { maxNumericValue: 200000 }],
        'resource-summary:total:size': ['error', { maxNumericValue: 500000 }],
      },
    },
  },
};

Lighthouse CI 的强大之处在于它可以测试真实的页面性能,包括首屏加载、资源瀑布流等完整指标。但它需要运行一个真实的服务器来托管页面,配置相对复杂。

4. RUM 监控系统

CI 中的性能检查只能保证构建产物在模拟环境下的表现,无法反映真实用户的多样性。RUM(Real User Monitoring)通过在实际用户浏览器中采集性能数据,让你能够看到真实用户在各种设备和网络条件下的体验。

数据采集架构

一个完整的 RUM 系统包括客户端数据采集、服务端数据接收、数据存储和数据可视化四个部分。

import { onLCP, onINP, onCLS, onFCP, onTTFB } from 'web-vitals';

class PerformanceMonitor {
  constructor(endpoint) {
    this.endpoint = endpoint;
    this.buffer = [];
    this.flushInterval = 5000;
    this.init();
  }

  init() {
    // 采集 Core Web Vitals
    onLCP(this.collectMetric.bind(this));
    onINP(this.collectMetric.bind(this));
    onCLS(this.collectMetric.bind(this));
    onFCP(this.collectMetric.bind(this));
    onTTFB(this.collectMetric.bind(this));

    // 采集资源加载数据
    this.collectResourceTiming();

    // 采集长任务
    this.collectLongTasks();

    // 定期上报
    setInterval(() => this.flush(), this.flushInterval);

    // 页面卸载时上报
    document.addEventListener('visibilitychange', () => {
      if (document.visibilityState === 'hidden') {
        this.flush();
      }
    });
  }

  collectMetric(metric) {
    this.buffer.push({
      type: 'web-vital',
      name: metric.name,
      value: metric.value,
      rating: metric.rating,
      delta: metric.delta,
      navigationType: metric.navigationType,
      url: location.href,
      timestamp: Date.now(),
      // 用户环境信息
      connection: navigator.connection?.effectiveType,
      deviceMemory: navigator.deviceMemory,
      hardwareConcurrency: navigator.hardwareConcurrency,
    });
  }

  collectLongTasks() {
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        this.buffer.push({
          type: 'long-task',
          duration: entry.duration,
          startTime: entry.startTime,
          url: location.href,
          timestamp: Date.now(),
        });
      }
    });
    observer.observe({ type: 'longtask', buffered: true });
  }

  collectResourceTiming() {
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (entry.transferSize > 50000) {
          this.buffer.push({
            type: 'large-resource',
            name: entry.name,
            transferSize: entry.transferSize,
            duration: entry.duration,
            initiatorType: entry.initiatorType,
          });
        }
      }
    });
    observer.observe({ type: 'resource', buffered: true });
  }

  flush() {
    if (this.buffer.length === 0) return;
    const data = this.buffer.splice(0);

    if (navigator.sendBeacon) {
      navigator.sendBeacon(this.endpoint, JSON.stringify(data));
    } else {
      fetch(this.endpoint, {
        method: 'POST',
        body: JSON.stringify(data),
        keepalive: true,
      });
    }
  }
}

// 初始化
const monitor = new PerformanceMonitor('/api/performance');

这个监控系统使用 web-vitals 库采集 Core Web Vitals,同时采集长任务和大型资源的数据。使用 sendBeacon API 确保即使页面卸载也能发送数据,避免数据丢失。

5. 报警与回归检测

采集到的性能数据需要被分析和使用才能发挥价值。性能报警系统可以在性能退化时及时通知团队,回归检测可以量化每次发布对性能的影响。

// 服务端:性能数据聚合与报警
class PerformanceAlertService {
  constructor(thresholds) {
    this.thresholds = thresholds;
  }

  async checkRegression(currentData, baselineData) {
    const alerts = [];

    for (const [metric, threshold] of Object.entries(this.thresholds)) {
      const current = this.getP75(currentData, metric);
      const baseline = this.getP75(baselineData, metric);
      const change = ((current - baseline) / baseline) * 100;

      if (current > threshold.absolute) {
        alerts.push({
          severity: 'critical',
          metric,
          message: `${metric} P75 (${current}ms) 超出绝对阈值 (${threshold.absolute}ms)`,
        });
      } else if (change > threshold.relativeChange) {
        alerts.push({
          severity: 'warning',
          metric,
          message: `${metric} P75 回退 ${change.toFixed(1)}%(${baseline}ms → ${current}ms)`,
        });
      }
    }

    return alerts;
  }

  getP75(data, metric) {
    const values = data
      .filter((d) => d.name === metric)
      .map((d) => d.value)
      .sort((a, b) => a - b);
    const index = Math.floor(values.length * 0.75);
    return values[index];
  }
}

const alertService = new PerformanceAlertService({
  LCP: { absolute: 2500, relativeChange: 10 },
  INP: { absolute: 200, relativeChange: 15 },
  CLS: { absolute: 0.1, relativeChange: 20 },
});

报警策略通常包括绝对阈值相对变化两个维度。绝对阈值用于判断性能是否处于不可接受的水平,相对变化用于检测性能是否在持续退化。即使性能值还在「可接受」范围内,如果相比基线出现明显退化,也应该引起关注。

6. 性能仪表盘

将性能数据可视化是让团队关注性能的有效方式。好的性能仪表盘应该展示关键指标的长期趋势,以及不同维度的细分数据。

// 构建性能仪表盘的数据查询
// 按时间段聚合 P50/P75/P95

const dashboardQueries = {
  // Core Web Vitals 趋势
  vitalsTrend: `
    SELECT
      date_trunc('hour', timestamp) as hour,
      metric_name,
      percentile_cont(0.50) WITHIN GROUP (ORDER BY value) as p50,
      percentile_cont(0.75) WITHIN GROUP (ORDER BY value) as p75,
      percentile_cont(0.95) WITHIN GROUP (ORDER BY value) as p95,
      count(*) as sample_count
    FROM performance_metrics
    WHERE timestamp > NOW() - INTERVAL '7 days'
    GROUP BY hour, metric_name
    ORDER BY hour
  `,

  // 按页面类型分析
  byPageType: `
    SELECT
      page_type,
      metric_name,
      percentile_cont(0.75) WITHIN GROUP (ORDER BY value) as p75,
      AVG(CASE WHEN rating = 'good' THEN 1.0 ELSE 0.0 END) as good_rate
    FROM performance_metrics
    WHERE timestamp > NOW() - INTERVAL '24 hours'
    GROUP BY page_type, metric_name
  `,

  // 按设备和网络分段
  bySegment: `
    SELECT
      connection_type,
      device_memory,
      metric_name,
      percentile_cont(0.75) WITHIN GROUP (ORDER BY value) as p75
    FROM performance_metrics
    WHERE timestamp > NOW() - INTERVAL '24 hours'
      AND metric_name = 'LCP'
    GROUP BY connection_type, device_memory, metric_name
  `,
};

7. 预算治理最佳实践

性能预算治理流程:

开发阶段:
├── Import Cost 插件 → 实时感知依赖体积
├── 构建预算检查 → 超出即报错
└── 本地 Lighthouse → 快速验证

PR 阶段:
├── size-limit CI → 评论体积变化
├── Lighthouse CI → 评论性能评分
└── 团队 Code Review → 关注性能影响

部署阶段:
├── 预发布环境测试 → Synthetic Monitoring
├── 灰度发布 → 对比新旧版本 RUM 数据
└── 全量发布 → 持续监控

运营阶段:
├── 每日巡检 → 自动化性能报告
├── 报警通知 → 超出阈值即告警
└── 季度回顾 → 调整预算目标

性能治理是一个持续的过程,需要在开发流程的每个环节都嵌入性能意识。开发阶段强调的是实时反馈,让开发者在编写代码时就能感知到性能影响。PR 阶段强调的是把关,确保每个变更都经过性能验证。部署阶段强调的是监控,及时发现发布后的问题。运营阶段强调的是迭代,根据数据调整目标和策略。


实战练习

练习 1:性能预算制定

为一个电商网站制定完整的性能预算方案,包括:确定主要用户群体的设备分布、设定各类页面的指标预算、制定资源体积预算、配置 Lighthouse CI 进行持续监控。

练习 2:CI 集成

在现有项目中配置 size-limit 和 Lighthouse CI,配置 GitHub Actions 工作流,确保每次 PR 都自动进行性能检查。设置当性能分数低于阈值时阻止合并。

练习 3:RUM 系统搭建

搭建一个简易的 RUM 采集和展示系统,包括:前端数据采集脚本、服务端数据接收 API、数据存储方案(可以使用 PostgreSQL 或简单的文件存储)、基础的数据可视化页面。使用真实用户数据验证系统工作正常。


延展阅读


关键术语

术语 解释
Performance Budget 性能预算,为性能指标或资源消耗设定的目标上限
RUM Real User Monitoring,真实用户监控,通过在用户浏览器中采集数据来了解实际体验
Synthetic Monitoring 合成监控,通过模拟用户行为在受控环境中进行性能测试
P75 / P95 第 75/95 百分位数,表示 75% 或 95% 的用户体验优于这个值
Regression 性能回归,版本迭代导致的性能退化
Baseline 基线数据,用于对比性能变化的参考值,通常是发布前的性能数据
size-limit 检查 JavaScript 库和应用体积的工具,可集成到 CI 中
Canary Deploy 灰度发布,逐步将新版本推送给部分用户以验证稳定性和性能