性能预算制定与管理完全指南

HTMLPAGE 团队
16 分钟阅读

学习如何为项目制定科学的性能预算,建立自动化监控机制,确保网站性能始终保持在可接受范围内,打造可持续的性能文化。

#性能预算 #性能监控 #Web Vitals #性能优化 #CI/CD

性能预算制定与管理完全指南

什么是性能预算

性能预算(Performance Budget)是为网站性能指标设定的量化限制。就像财务预算控制支出一样,性能预算控制着网站的"性能支出"。

性能预算的核心概念:

┌─────────────────────────────────────────────────────────────┐
│                      性能预算                               │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  指标类型          预算值           当前值          状态    │
│  ─────────────────────────────────────────────────────────  │
│  📦 JS Bundle      < 200KB          185KB           ✅     │
│  🎨 CSS Bundle     < 50KB           48KB            ✅     │
│  🖼️ 图片总大小     < 1MB            1.2MB           ❌     │
│  ⏱️ LCP            < 2.5s           2.1s            ✅     │
│  ⚡ FID            < 100ms          85ms            ✅     │
│  📐 CLS            < 0.1            0.15            ❌     │
│  🚀 TTI            < 3.8s           4.2s            ❌     │
│                                                             │
│  总体状态: 4/7 通过 (57%)  需要优化!                       │
└─────────────────────────────────────────────────────────────┘

为什么需要性能预算

1. 预防性能退化

没有预算限制,性能会像无预算的项目一样失控:

典型的性能退化曲线:

性能
得分
100 │
 90 │ ●
 80 │   ●
 70 │     ●  新功能A
 60 │       ●  新功能B
 50 │          ●  第三方SDK
 40 │             ●  埋点代码
 30 │                ●  A/B测试
 20 │                   ●
    └─────────────────────────→ 时间

有性能预算后:

性能
得分
100 │
 90 │ ●──●──●──●──●──●──●──●  ← 预算线保护
 80 │
    └─────────────────────────→ 时间

2. 建立性能文化

性能预算让性能成为可量化、可追踪的目标,而非模糊的"感觉快"。

3. 在功能和性能间取得平衡

当资源有限时,预算帮助团队做出取舍决策。

性能预算的类型

1. 基于时间的预算

// 时间预算配置
const timeBasedBudget = {
  // 加载时间指标
  metrics: {
    // 首次内容绘制
    FCP: {
      budget: 1800, // ms
      good: 1000,
      needsImprovement: 3000
    },
    
    // 最大内容绘制
    LCP: {
      budget: 2500, // ms
      good: 2500,
      needsImprovement: 4000
    },
    
    // 可交互时间
    TTI: {
      budget: 3800, // ms
      good: 3800,
      needsImprovement: 7300
    },
    
    // 总阻塞时间
    TBT: {
      budget: 200, // ms
      good: 200,
      needsImprovement: 600
    },
    
    // 首次输入延迟
    FID: {
      budget: 100, // ms
      good: 100,
      needsImprovement: 300
    }
  },
  
  // 评估方法
  evaluate(measured) {
    const results = {};
    for (const [metric, config] of Object.entries(this.metrics)) {
      const value = measured[metric];
      results[metric] = {
        value,
        budget: config.budget,
        passed: value <= config.budget,
        status: value <= config.good ? 'good' : 
                value <= config.needsImprovement ? 'needs-improvement' : 'poor'
      };
    }
    return results;
  }
};

2. 基于资源大小的预算

// 资源大小预算配置
const sizeBasedBudget = {
  // 按资源类型
  resourceTypes: {
    script: {
      budget: 300 * 1024,     // 300KB
      description: 'JavaScript 总大小'
    },
    stylesheet: {
      budget: 100 * 1024,     // 100KB
      description: 'CSS 总大小'
    },
    image: {
      budget: 500 * 1024,     // 500KB
      description: '图片总大小'
    },
    font: {
      budget: 100 * 1024,     // 100KB
      description: '字体总大小'
    },
    total: {
      budget: 1024 * 1024,    // 1MB
      description: '页面总大小'
    }
  },
  
  // 按入口文件
  entryPoints: {
    'main.js': {
      budget: 150 * 1024,     // 150KB
      description: '主 Bundle'
    },
    'vendor.js': {
      budget: 100 * 1024,     // 100KB
      description: '第三方库 Bundle'
    }
  },
  
  // 按路由
  routes: {
    '/': {
      script: 200 * 1024,
      total: 800 * 1024
    },
    '/dashboard': {
      script: 300 * 1024,
      total: 1024 * 1024
    }
  }
};

3. 基于数量的预算

// 数量预算配置
const countBasedBudget = {
  // HTTP 请求数
  requests: {
    script: { budget: 10, description: 'JS 请求数' },
    stylesheet: { budget: 5, description: 'CSS 请求数' },
    image: { budget: 20, description: '图片请求数' },
    font: { budget: 4, description: '字体请求数' },
    total: { budget: 50, description: '总请求数' }
  },
  
  // DOM 元素数
  domElements: {
    budget: 1500,
    warning: 1000,
    description: 'DOM 节点总数'
  },
  
  // 第三方脚本
  thirdPartyScripts: {
    budget: 5,
    description: '第三方脚本数量'
  },
  
  // 自定义字体
  customFonts: {
    budget: 3,
    description: '自定义字体数量'
  }
};

4. 基于规则的预算

// 规则预算配置
const ruleBasedBudget = {
  rules: [
    {
      id: 'no-unminified-js',
      description: '所有 JS 必须压缩',
      check: (resources) => {
        const jsFiles = resources.filter(r => r.type === 'script');
        return jsFiles.every(f => !f.name.includes('.min.') || f.isMinified);
      }
    },
    {
      id: 'images-optimized',
      description: '图片必须使用现代格式',
      check: (resources) => {
        const images = resources.filter(r => r.type === 'image');
        const modernFormats = ['webp', 'avif'];
        return images.every(img => 
          modernFormats.some(fmt => img.name.endsWith(`.${fmt}`))
        );
      }
    },
    {
      id: 'no-blocking-resources',
      description: '关键路径不能有阻塞资源',
      check: (resources) => {
        return resources
          .filter(r => r.isRenderBlocking)
          .length === 0;
      }
    },
    {
      id: 'font-display-swap',
      description: '字体必须使用 font-display: swap',
      check: (stylesheets) => {
        return stylesheets.every(css => 
          !css.content.includes('@font-face') || 
          css.content.includes('font-display: swap')
        );
      }
    }
  ]
};

如何制定合理的性能预算

步骤1:确定业务目标

// 性能与业务目标对应关系
const businessGoals = {
  // 电商网站
  ecommerce: {
    // 转化率每秒下降 7%
    conversionRateImpact: 0.07,
    
    // 关键指标
    criticalMetrics: ['LCP', 'FID', 'CLS'],
    
    // 预算策略
    budgetStrategy: {
      LCP: 2000,    // 产品图片快速展示
      FID: 50,      // 添加购物车响应快
      CLS: 0.05     // 避免布局抖动影响点击
    }
  },
  
  // 内容网站
  content: {
    // 跳出率每秒增加 10%
    bounceRateImpact: 0.10,
    
    criticalMetrics: ['FCP', 'LCP', 'TTI'],
    
    budgetStrategy: {
      FCP: 1000,    // 内容快速可见
      LCP: 2000,    // 主图片/标题快速加载
      TTI: 3000     // 快速可阅读
    }
  },
  
  // SaaS 应用
  saas: {
    criticalMetrics: ['TTI', 'FID', 'TBT'],
    
    budgetStrategy: {
      TTI: 4000,    // 可接受较长初始加载
      FID: 50,      // 交互必须流畅
      TBT: 150      // 操作时不能卡顿
    }
  }
};

步骤2:分析竞争对手

// 竞品性能分析工具
async function analyzeCompetitors(competitors) {
  const results = {};
  
  for (const url of competitors) {
    try {
      // 使用 PageSpeed Insights API
      const response = await fetch(
        `https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=${encodeURIComponent(url)}&strategy=mobile`
      );
      const data = await response.json();
      
      const metrics = data.lighthouseResult.audits;
      
      results[url] = {
        FCP: metrics['first-contentful-paint'].numericValue,
        LCP: metrics['largest-contentful-paint'].numericValue,
        TTI: metrics['interactive'].numericValue,
        TBT: metrics['total-blocking-time'].numericValue,
        CLS: metrics['cumulative-layout-shift'].numericValue,
        score: data.lighthouseResult.categories.performance.score * 100
      };
    } catch (error) {
      results[url] = { error: error.message };
    }
  }
  
  // 计算中位数和最佳值
  const allMetrics = Object.values(results).filter(r => !r.error);
  
  return {
    competitors: results,
    benchmarks: {
      FCP: {
        median: median(allMetrics.map(m => m.FCP)),
        best: Math.min(...allMetrics.map(m => m.FCP)),
        target: Math.min(...allMetrics.map(m => m.FCP)) * 0.8 // 比最好的还快 20%
      },
      LCP: {
        median: median(allMetrics.map(m => m.LCP)),
        best: Math.min(...allMetrics.map(m => m.LCP)),
        target: Math.min(...allMetrics.map(m => m.LCP)) * 0.8
      }
      // ... 其他指标
    }
  };
}

function median(arr) {
  const sorted = [...arr].sort((a, b) => a - b);
  const mid = Math.floor(sorted.length / 2);
  return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
}

步骤3:确定预算阈值

// 性能预算计算器
class PerformanceBudgetCalculator {
  constructor(config) {
    this.businessType = config.businessType;
    this.targetUsers = config.targetUsers;
    this.currentMetrics = config.currentMetrics;
    this.competitorBenchmarks = config.competitorBenchmarks;
  }
  
  calculate() {
    const budget = {};
    
    // Core Web Vitals 预算
    budget.webVitals = this.calculateWebVitalsBudget();
    
    // 资源预算
    budget.resources = this.calculateResourceBudget();
    
    // 请求预算
    budget.requests = this.calculateRequestBudget();
    
    return budget;
  }
  
  calculateWebVitalsBudget() {
    // 基于 Google 推荐阈值
    const googleThresholds = {
      LCP: { good: 2500, poor: 4000 },
      FID: { good: 100, poor: 300 },
      CLS: { good: 0.1, poor: 0.25 },
      FCP: { good: 1800, poor: 3000 },
      TTI: { good: 3800, poor: 7300 },
      TBT: { good: 200, poor: 600 }
    };
    
    const budget = {};
    
    for (const [metric, thresholds] of Object.entries(googleThresholds)) {
      const current = this.currentMetrics[metric];
      const competitor = this.competitorBenchmarks?.[metric]?.best;
      
      // 取 Google 推荐值和竞品最佳值的较小者
      const target = competitor 
        ? Math.min(thresholds.good, competitor * 0.9)
        : thresholds.good;
      
      budget[metric] = {
        target: Math.round(target),
        current,
        improvement: current ? Math.round((current - target) / current * 100) : null,
        priority: current > thresholds.poor ? 'critical' : 
                  current > thresholds.good ? 'high' : 'normal'
      };
    }
    
    return budget;
  }
  
  calculateResourceBudget() {
    // 基于目标设备和网络条件计算
    const networkProfiles = {
      '4g': {
        bandwidth: 10 * 1024 * 1024 / 8, // 10 Mbps = 1.25 MB/s
        rtt: 50  // 50ms
      },
      '3g': {
        bandwidth: 1.5 * 1024 * 1024 / 8, // 1.5 Mbps = 187.5 KB/s
        rtt: 400 // 400ms
      }
    };
    
    const targetNetwork = this.targetUsers.network || '4g';
    const profile = networkProfiles[targetNetwork];
    
    // 目标:3秒内完成关键资源加载
    const loadTimeTarget = 3000; // ms
    const availableBytes = profile.bandwidth * (loadTimeTarget / 1000);
    
    return {
      total: {
        target: Math.round(availableBytes),
        description: `${(availableBytes / 1024).toFixed(0)}KB 总大小目标`
      },
      javascript: {
        target: Math.round(availableBytes * 0.35), // JS 占 35%
        description: 'JavaScript 预算'
      },
      css: {
        target: Math.round(availableBytes * 0.10), // CSS 占 10%
        description: 'CSS 预算'
      },
      images: {
        target: Math.round(availableBytes * 0.40), // 图片占 40%
        description: '图片预算'
      },
      fonts: {
        target: Math.round(availableBytes * 0.10), // 字体占 10%
        description: '字体预算'
      },
      other: {
        target: Math.round(availableBytes * 0.05), // 其他占 5%
        description: '其他资源'
      }
    };
  }
  
  calculateRequestBudget() {
    // HTTP/2 多路复用下的合理请求数
    return {
      critical: {
        target: 10,
        description: '关键路径请求数'
      },
      total: {
        target: 50,
        description: '总请求数'
      },
      thirdParty: {
        target: 5,
        description: '第三方脚本数'
      }
    };
  }
}

// 使用示例
const calculator = new PerformanceBudgetCalculator({
  businessType: 'ecommerce',
  targetUsers: {
    network: '4g',
    device: 'mobile'
  },
  currentMetrics: {
    LCP: 3200,
    FID: 150,
    CLS: 0.12
  },
  competitorBenchmarks: {
    LCP: { best: 2100 },
    FID: { best: 80 }
  }
});

const budget = calculator.calculate();
console.log(budget);

性能预算自动化监控

Webpack 配置

// webpack.config.js
module.exports = {
  performance: {
    // 资产大小限制
    maxAssetSize: 200 * 1024,         // 200KB
    maxEntrypointSize: 400 * 1024,    // 400KB
    
    // 超出预算时的行为:error | warning
    hints: process.env.NODE_ENV === 'production' ? 'error' : 'warning',
    
    // 自定义资产过滤
    assetFilter: function(assetFilename) {
      // 只检查 JS 和 CSS
      return /\.(js|css)$/.test(assetFilename);
    }
  },
  
  plugins: [
    // 使用 bundlesize 插件进行更细粒度控制
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      reportFilename: 'bundle-report.html',
      openAnalyzer: false
    })
  ]
};

bundlesize 配置

// package.json
{
  "bundlesize": [
    {
      "path": "./dist/js/main.*.js",
      "maxSize": "150 kB",
      "compression": "gzip"
    },
    {
      "path": "./dist/js/vendor.*.js", 
      "maxSize": "100 kB",
      "compression": "gzip"
    },
    {
      "path": "./dist/css/main.*.css",
      "maxSize": "30 kB",
      "compression": "gzip"
    },
    {
      "path": "./dist/js/*.js",
      "maxSize": "300 kB",
      "compression": "none"
    }
  ],
  "scripts": {
    "check-size": "bundlesize"
  }
}

Lighthouse CI 配置

// lighthouserc.js
module.exports = {
  ci: {
    collect: {
      // 收集配置
      numberOfRuns: 3,
      url: [
        'http://localhost:3000/',
        'http://localhost:3000/products',
        'http://localhost:3000/checkout'
      ],
      settings: {
        preset: 'desktop',
        throttling: {
          rttMs: 40,
          throughputKbps: 10240,
          cpuSlowdownMultiplier: 1
        }
      }
    },
    
    assert: {
      // 预算断言
      preset: 'lighthouse:recommended',
      
      assertions: {
        // Core Web Vitals
        'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
        'first-contentful-paint': ['error', { maxNumericValue: 1800 }],
        'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
        'total-blocking-time': ['error', { maxNumericValue: 200 }],
        
        // 性能评分
        'categories:performance': ['error', { minScore: 0.9 }],
        
        // 资源大小
        'resource-summary:script:size': ['error', { maxNumericValue: 300000 }],
        'resource-summary:stylesheet:size': ['error', { maxNumericValue: 100000 }],
        'resource-summary:image:size': ['error', { maxNumericValue: 500000 }],
        
        // 最佳实践
        'uses-responsive-images': 'error',
        'uses-webp-images': 'warn',
        'render-blocking-resources': 'warn',
        'unused-javascript': 'warn'
      }
    },
    
    upload: {
      target: 'temporary-public-storage'
    }
  }
};

GitHub Actions 集成

# .github/workflows/performance-budget.yml
name: Performance Budget Check

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

jobs:
  bundle-size:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Build
        run: npm run build
      
      - name: Check bundle size
        run: npx bundlesize
        env:
          BUNDLESIZE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Build
        run: npm run build
      
      - name: Start server
        run: npm run preview &
        
      - name: Wait for server
        run: npx wait-on http://localhost:3000
      
      - name: Run Lighthouse CI
        run: |
          npm install -g @lhci/cli
          lhci autorun
        env:
          LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
      
      - name: Upload Lighthouse report
        uses: actions/upload-artifact@v3
        if: always()
        with:
          name: lighthouse-report
          path: .lighthouseci/

  web-vitals:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
      
      - name: Measure Web Vitals
        uses: preactjs/compressed-size-action@v2
        with:
          repo-token: ${{ secrets.GITHUB_TOKEN }}
          build-script: "build"
          pattern: "./dist/**/*.{js,css,html}"

自定义预算检查脚本

// scripts/check-performance-budget.js
const fs = require('fs');
const path = require('path');
const gzipSize = require('gzip-size');
const chalk = require('chalk');

// 预算配置
const BUDGET = {
  resources: {
    'dist/js/main.*.js': { maxSize: 150 * 1024, gzip: true },
    'dist/js/vendor.*.js': { maxSize: 100 * 1024, gzip: true },
    'dist/css/*.css': { maxSize: 50 * 1024, gzip: true }
  },
  totals: {
    javascript: { maxSize: 300 * 1024, gzip: true },
    css: { maxSize: 80 * 1024, gzip: true },
    images: { maxSize: 500 * 1024, gzip: false },
    all: { maxSize: 1024 * 1024, gzip: false }
  }
};

class BudgetChecker {
  constructor(distPath, budget) {
    this.distPath = distPath;
    this.budget = budget;
    this.results = [];
    this.passed = true;
  }
  
  async check() {
    console.log(chalk.blue('\n📊 性能预算检查\n'));
    console.log(chalk.gray('─'.repeat(60)));
    
    // 检查单个资源
    await this.checkResources();
    
    // 检查总量
    await this.checkTotals();
    
    // 输出结果
    this.printResults();
    
    return this.passed;
  }
  
  async checkResources() {
    for (const [pattern, config] of Object.entries(this.budget.resources)) {
      const files = this.findFiles(pattern);
      
      for (const file of files) {
        const size = await this.getSize(file, config.gzip);
        const passed = size <= config.maxSize;
        
        if (!passed) this.passed = false;
        
        this.results.push({
          type: 'resource',
          name: path.relative(this.distPath, file),
          size,
          maxSize: config.maxSize,
          gzip: config.gzip,
          passed
        });
      }
    }
  }
  
  async checkTotals() {
    const categories = {
      javascript: /\.js$/,
      css: /\.css$/,
      images: /\.(png|jpg|jpeg|gif|webp|avif|svg)$/i
    };
    
    const allFiles = this.findFiles('**/*');
    
    for (const [category, regex] of Object.entries(categories)) {
      const files = allFiles.filter(f => regex.test(f));
      let totalSize = 0;
      
      for (const file of files) {
        const config = this.budget.totals[category];
        totalSize += await this.getSize(file, config?.gzip || false);
      }
      
      const config = this.budget.totals[category];
      if (config) {
        const passed = totalSize <= config.maxSize;
        if (!passed) this.passed = false;
        
        this.results.push({
          type: 'total',
          name: `Total ${category}`,
          size: totalSize,
          maxSize: config.maxSize,
          gzip: config.gzip,
          passed
        });
      }
    }
  }
  
  findFiles(pattern) {
    const glob = require('glob');
    return glob.sync(path.join(this.distPath, pattern));
  }
  
  async getSize(filePath, gzip) {
    const content = fs.readFileSync(filePath);
    return gzip ? await gzipSize(content) : content.length;
  }
  
  formatSize(bytes) {
    if (bytes < 1024) return `${bytes} B`;
    if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
    return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
  }
  
  printResults() {
    console.log(chalk.gray('\n资源检查结果:\n'));
    
    const resourceResults = this.results.filter(r => r.type === 'resource');
    const totalResults = this.results.filter(r => r.type === 'total');
    
    // 单个资源
    for (const result of resourceResults) {
      const status = result.passed ? chalk.green('✓') : chalk.red('✗');
      const sizeText = this.formatSize(result.size);
      const budgetText = this.formatSize(result.maxSize);
      const gzipLabel = result.gzip ? chalk.gray('(gzip)') : '';
      
      const percentage = ((result.size / result.maxSize) * 100).toFixed(1);
      const bar = this.progressBar(result.size, result.maxSize);
      
      console.log(
        `${status} ${result.name}`,
        chalk.gray(`${sizeText} / ${budgetText} ${gzipLabel}`),
        bar,
        chalk.gray(`${percentage}%`)
      );
    }
    
    console.log(chalk.gray('\n总量检查结果:\n'));
    
    // 总量
    for (const result of totalResults) {
      const status = result.passed ? chalk.green('✓') : chalk.red('✗');
      const sizeText = this.formatSize(result.size);
      const budgetText = this.formatSize(result.maxSize);
      
      console.log(
        `${status} ${result.name}:`,
        chalk.gray(`${sizeText} / ${budgetText}`)
      );
    }
    
    // 总结
    console.log(chalk.gray('\n' + '─'.repeat(60)));
    
    const passedCount = this.results.filter(r => r.passed).length;
    const totalCount = this.results.length;
    
    if (this.passed) {
      console.log(chalk.green(`\n✅ 所有检查通过 (${passedCount}/${totalCount})\n`));
    } else {
      console.log(chalk.red(`\n❌ 预算超标 (${passedCount}/${totalCount} 通过)\n`));
    }
  }
  
  progressBar(current, max) {
    const width = 20;
    const percentage = Math.min(current / max, 1);
    const filled = Math.round(width * percentage);
    const empty = width - filled;
    
    const color = percentage > 1 ? chalk.red : 
                  percentage > 0.8 ? chalk.yellow : chalk.green;
    
    return color('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
  }
}

// 执行检查
const checker = new BudgetChecker('./dist', BUDGET);
checker.check().then(passed => {
  process.exit(passed ? 0 : 1);
});

预算超标时的应对策略

分析超标原因

// 分析 Bundle 大小的常见问题
const bundleAnalysis = {
  // 问题1:重复依赖
  duplicateDependencies: {
    detect: 'webpack-bundle-analyzer',
    solution: `
      // 使用 resolutions 强制统一版本
      // package.json
      {
        "resolutions": {
          "lodash": "^4.17.21"
        }
      }
    `
  },
  
  // 问题2:全量引入库
  fullImports: {
    example: "import _ from 'lodash'",
    solution: "import debounce from 'lodash/debounce'"
  },
  
  // 问题3:开发依赖打包
  devDependenciesIncluded: {
    detect: '检查 bundle 中是否有 test / mock / storybook 代码',
    solution: '配置正确的 sideEffects 和 tree-shaking'
  },
  
  // 问题4:未压缩的资源
  uncompressedResources: {
    solution: '启用 gzip/brotli 压缩'
  }
};

// Bundle 大小优化清单
const optimizationChecklist = [
  {
    category: '代码分割',
    items: [
      '路由级别代码分割',
      '组件懒加载',
      '第三方库单独打包'
    ]
  },
  {
    category: '依赖优化',
    items: [
      '移除未使用的依赖',
      '使用轻量替代品(如 date-fns 替代 moment)',
      'Tree Shaking 确认生效'
    ]
  },
  {
    category: '资源优化',
    items: [
      '图片使用 WebP/AVIF 格式',
      '字体子集化',
      'SVG 优化'
    ]
  },
  {
    category: '构建优化',
    items: [
      '启用压缩(Terser/ESBuild)',
      '移除 Source Maps(生产环境)',
      '使用生产模式构建'
    ]
  }
];

预算调整流程

预算超标处理流程:

    ┌───────────────┐
    │  预算超标报警  │
    └───────┬───────┘
            ↓
    ┌───────────────────────────────────────┐
    │  1. 确认是否为预期变更               │
    │     - 新功能导致的合理增长?          │
    │     - 还是意外引入的问题?            │
    └───────────────────┬───────────────────┘
                        ↓
        ┌───────────────┴───────────────┐
        ↓                               ↓
    预期增长                         意外问题
        │                               │
        ↓                               ↓
┌───────────────────┐           ┌───────────────────┐
│ 2A. 评估是否值得  │           │ 2B. 调查根因      │
│   - ROI 分析      │           │   - 依赖分析      │
│   - 用户价值      │           │   - 代码审查      │
└─────────┬─────────┘           └─────────┬─────────┘
          ↓                               ↓
    ┌─────────────────┐           ┌─────────────────┐
    │ 3A. 调整预算    │           │ 3B. 修复问题    │
    │   + 记录原因    │           │   + 防止复发    │
    └─────────────────┘           └─────────────────┘

团队性能文化建设

建立性能意识

// 性能看板配置
const performanceDashboard = {
  // 核心指标
  keyMetrics: [
    { name: 'LCP', current: 2100, budget: 2500, trend: 'improving' },
    { name: 'FID', current: 85, budget: 100, trend: 'stable' },
    { name: 'CLS', current: 0.08, budget: 0.1, trend: 'improving' }
  ],
  
  // Bundle 大小趋势
  bundleTrend: {
    labels: ['1月', '2月', '3月', '4月', '5月'],
    data: [180, 195, 205, 198, 185], // KB
    budget: 200
  },
  
  // 告警配置
  alerts: {
    // Slack 通知
    slack: {
      channel: '#performance',
      triggers: ['budget-exceeded', 'score-drop']
    },
    // 邮件通知
    email: {
      recipients: ['dev-team@company.com'],
      frequency: 'weekly'
    }
  }
};

代码审查清单

## 性能审查清单

### Bundle 大小
- [ ] 新依赖是否必要?有更轻量的替代品吗?
- [ ] 是否使用了按需加载?
- [ ] 是否有重复的依赖?

### 运行时性能
- [ ] 是否有不必要的重渲染?
- [ ] 是否有内存泄漏风险?
- [ ] 事件监听器是否正确清理?

### 资源加载
- [ ] 图片是否优化?格式是否现代化?
- [ ] 是否使用了懒加载?
- [ ] 关键资源是否预加载?

### 网络请求
- [ ] API 请求是否合理?是否可以合并?
- [ ] 是否有不必要的第三方脚本?

最佳实践总结

性能预算最佳实践:

制定阶段:
✓ 基于业务目标设定预算,不要凭感觉
✓ 分析竞争对手,确保有竞争力
✓ 考虑目标用户的设备和网络条件
✓ 预留 10-20% 的缓冲空间

实施阶段:
✓ 集成到 CI/CD 流程中自动检查
✓ 超出预算时阻止部署或发出告警
✓ 使用多种工具交叉验证
✓ 定期审查和调整预算

维护阶段:
✓ 每季度回顾预算是否合理
✓ 随着业务发展调整预算
✓ 记录每次预算调整的原因
✓ 建立性能报告和趋势追踪

团队协作:
✓ 让全团队了解性能预算
✓ 在代码审查中检查性能影响
✓ 庆祝性能改进,关注性能回归
✓ 建立性能冠军/负责人制度

性能预算不是限制,而是保护。它帮助团队在快速迭代中保持性能底线,让用户始终获得良好的体验。