JavaScript性能优化实战(13):性能测试与持续优化

发布于:2025-05-21 ⋅ 阅读:(15) ⋅ 点赞:(0)

在前面的系列文章中,我们探讨了各种JavaScript性能优化的方法和实战案例。然而,优化工作不应仅是一次性的努力,而应当成为开发流程中的常态。本篇将聚焦于如何建立系统化的性能测试体系,并实现持续的性能优化机制,确保应用长期保持出色的性能表现。

前端性能测试体系构建

随着前端应用日益复杂,系统性能对用户体验和业务成功的影响越来越大。然而,许多团队仍然采用临时性的、非系统化的方式进行性能测试和优化。建立一个完善的前端性能测试体系,对于持续交付高性能应用至关重要。

性能测试的核心维度

完整的前端性能测试体系应当覆盖以下几个核心维度:

  1. 加载性能:衡量应用从请求到可用所需的时间
  2. 运行时性能:评估应用在用户交互过程中的响应性和流畅度
  3. 内存使用:监控应用的内存占用情况和潜在的内存泄漏
  4. 网络效率:测量应用的网络请求数量、体积和时序
  5. 能耗性能:特别是在移动设备上,应用对电池寿命的影响

建立性能指标体系

性能测试的第一步是确定关键指标,这些指标应当既有技术维度的客观数据,也有用户体验的主观反映。

核心Web Vitals指标

Google的Core Web Vitals提供了衡量用户体验的标准化指标:

// 使用Web Vitals库收集核心指标
import {
   
  getCLS,
  getFID,
  getLCP,
  getFCP,
  getTTFB
} from 'web-vitals';

// 发送性能数据到分析服务
function sendToAnalytics({
    name, delta, value, id}) {
   
  const analyticsData = {
   
    metric: name,
    value: delta, // 增量值
    finalValue: value, // 最终值
    id: id, // 唯一标识符
    page: window.location.pathname,
    timestamp: Date.now()
  };
  
  // 发送到分析服务
  navigator.sendBeacon('/analytics', JSON.stringify(analyticsData));
}

// 监测并报告各项指标
getCLS(sendToAnalytics);  // 累积布局偏移
getFID(sendToAnalytics);  // 首次输入延迟
getLCP(sendToAnalytics);  // 最大内容绘制时间
getFCP(sendToAnalytics);  // 首次内容绘制时间
getTTFB(sendToAnalytics); // 首字节时间
自定义业务相关指标

除了标准化指标外,还应针对业务特点建立自定义指标,例如:

  • 关键业务流程性能:如电商网站的商品浏览到下单完成的全流程时间
  • 特定交互响应性:如复杂数据可视化应用中图表渲染和交互的响应时间
  • 长任务频率:应用中阻塞主线程超过50ms的任务数量和分布
// 自定义业务流程性能监测
class BusinessFlowPerformance {
   
  constructor(flowName) {
   
    this.flowName = flowName;
    this.steps = {
   };
    this.startTime = 0;
    this.endTime = 0;
  }

  startFlow() {
   
    this.startTime = performance.now();
    console.log(`Flow ${
     this.flowName} started`);
  }

  recordStep(stepName) {
   
    this.steps[stepName] = performance.now() - this.startTime;
    console.log(`Step ${
     stepName} completed at ${
     this.steps[stepName]}ms`);
  }

  endFlow() {
   
    this.endTime = performance.now();
    const totalDuration = this.endTime - this.startTime;
    console.log(`Flow ${
     this.flowName} completed in ${
     totalDuration}ms`);
    
    // 发送完整流程数据
    this.sendFlowData({
   
      flowName: this.flowName,
      totalDuration,
      steps: this.steps
    });
  }

  sendFlowData(data) {
   
    // 发送到数据分析服务
    fetch('/api/performance/business-flow', {
   
      method: 'POST',
      body: JSON.stringify(data),
      headers: {
    'Content-Type': 'application/json' }
    });
  }
}

// 使用示例
const checkoutFlow = new BusinessFlowPerformance('checkout');
checkoutFlow.startFlow();

// 在各关键步骤调用
// 用户点击结算按钮
checkoutFlow.recordStep('cart_to_checkout');

// 地址填写完成
checkoutFlow.recordStep('address_completed');

// 支付方式选择完成
checkoutFlow.recordStep('payment_selected');

// 订单确认
checkoutFlow.recordStep('order_confirmed');

// 流程完成
checkoutFlow.endFlow();

性能测试环境构建

有效的性能测试需要在标准化、可控的环境中进行,以确保结果的可重复性和可比性。

实验室测试环境设置

实验室测试环境应模拟真实的用户设备和网络条件:

// 使用Puppeteer进行性能测试的示例配置
const puppeteer = require('puppeteer');

async function runPerformanceTest(url, devicePreset, networkPreset) {
   
  // 启动浏览器
  const browser = await puppeteer.launch({
   
    headless: true,
    args: ['--no-sandbox', '--disable-setuid-sandbox']
  });
  
  // 创建新页面
  const page = await browser.newPage();
  
  // 设置设备模拟
  await page.emulate(puppeteer.devices[devicePreset]);
  
  // 模拟网络条件
  const client = await page.target().createCDPSession();
  await client.send('Network.enable');
  
  // 预设的网络配置
  const networkConfigs = {
   
    '3G': {
   
      'offline': false,
      'downloadThroughput': 750 * 1024 / 8,
      'uploadThroughput': 250 * 1024 / 8,
      'latency': 100
    },
    '4G': {
   
      'offline': false,
      'downloadThroughput': 4 * 1024 * 1024 / 8,
      'uploadThroughput': 2 * 1024 * 1024 / 8,
      'latency': 50
    }
  };
  
  await client.send('Network.emulateNetworkConditions', 
    networkConfigs[networkPreset] || networkConfigs['4G']
  );
  
  // 收集性能指标
  await page.evaluateOnNewDocument(() => {
   
    window.performanceMetrics = {
   
      FCP: 0,
      LCP: 0,
      CLS: 0,
      TTI: 0
    };
    
    // 首次内容绘制
    new PerformanceObserver((entryList) => {
   
      const entries = entryList.getEntries();
      const fcpEntry = entries[entries.length - 1];
      window.performanceMetrics.FCP = fcpEntry.startTime;
    }).observe({
    type: 'paint', buffered: true });
    
    // 最大内容绘制
    new PerformanceObserver((entryList) => {
   
      const entries = entryList.getEntries();
      const lcpEntry = entries[entries.length - 1];
      window.performanceMetrics.LCP = lcpEntry.startTime;
    }).observe({
    type: 'largest-contentful-paint', buffered: true });
    
    // 累积布局偏移
    new PerformanceObserver((entryList) => {
   
      let cumulativeScore = 0;
      for (const entry of entryList.getEntries()) {
   
        // 只计算没有用户输入的布局偏移
        if (!entry.hadRecentInput) {
   
          cumulativeScore += entry.value;
        }
      }
      window.performanceMetrics.CLS = cumulativeScore;
    }).observe({
    type: 'layout-shift', buffered: true });
  });
  
  // 访问目标页面并等待网络空闲
  const response = await page.goto(url, {
   
    waitUntil: 'networkidle2'
  });
  
  // 等待页面完全加载并可交互
  await page.waitForSelector('#main-content', {
    visible: true });
  
  // 提取性能指标
  const metrics = await page.evaluate(() => {
   
    // 等待TTI计算完成
    return new Promise(resolve => {
   
      // 模拟TTI计算
      setTimeout(() => {
   
        const navigationStart = performance.timing.navigationStart;
        const loadEventEnd = performance.timing.loadEventEnd;
        
        window.performanceMetrics.TTI = loadEventEnd - navigationStart;
        
        resolve(window.performanceMetrics);
      }, 1000);
    });
  });
  
  // 截图以备记录
  await page.screenshot({
    path: `${
     devicePreset}-${
     networkPreset}.png` });
  
  // 关闭浏览器
  await browser.close();
  
  return {
   
    url,
    devicePreset,
    networkPreset,
    metrics,
    statusCode: response.status()
  };
}

// 使用实例
async function runBatchTests() {
   
  const results = [];
  
  // 测试不同设备和网络组合
  const devices = ['iPhone X', 'Pixel 2', 'Desktop Chrome'];
  const networks = ['3G', '4G'];
  const urls = [
    'https://example.com',
    'https://example.com/products',
    'https://example.com/checkout'
  ];
  
  for (const url of urls) {
   
    for (const device of devices) {
   
      for (const network of networks) {
   
        const result = await runPerformanceTest(url, device, network);
        results.push(result);
      }
    }
  }
  
  // 生成性能测试报告
  generatePerformanceReport(results);
}

#### 真实用户监测(RUM)设置

实验室测试需要与真实用户数据相结合,才能全面反映应用性能:

```javascript
// 真实用户监测脚本示例
(function() {
   
  // 避免在非浏览器环境运行
  if (typeof window === 'undefined') return;
  
  // 确保Performance API可用
  if (!window.performance || 
      !window.performance.timing || 
      !window.performance.getEntriesByType) {
   
    console.warn('Performance API不完全支持,RUM数据可能不完整');
  }
  
  // 基础配置
  const config = {
   
    sampleRate: 0.1, // 采样率10%
    beaconEndpoint: '/analytics/rum',
    appVersion: '1.2.3',
    environment: 'production'
  };
  
  // 只对采样用户进行监测
  if (Math.random() > config.sampleRate) return;
  
  // 收集设备和浏览器信息
  const deviceInfo = {
   
    userAgent: navigator.userAgent,
    viewport: {
   
      width: window.innerWidth,
      height: window.innerHeight
    },
    devicePixelRatio: window.devicePixelRatio || 1,
    connection: navigator.connection ? {
   
      effectiveType: navigator.connection.effectiveType,
      downlink: navigator.connection.downlink,
      rtt: navigator.connection.rtt
    } : null
  };
  
  // 收集导航性能数据
  function collectNavigationTiming() {
   
    // 使用Performance Timeline API
    const navEntry = performance.getEntriesByType('navigation')[0];
    
    if (navEntry) {
   
      return {
   
        startTime: navEntry.startTime,
        domContentLoaded: navEntry.domContentLoadedEventEnd,
        loadTime: navEntry.loadEventEnd,
        domInteractive: navEntry.domInteractive,
        redirectCount: navEntry.redirectCount,
        redirectTime: navEntry.redirectEnd - navEntry.redirectStart,
        dnsTime: navEntry.domainLookupEnd - navEntry.domainLookupStart,
        tcpTime: navEntry.connectEnd - navEntry.connectStart,
        ttfb: navEntry.responseStart - navEntry.requestStart,
        responseTime: navEntry.responseEnd - navEntry.responseStart,
        domBuildingTime: navEntry.domComplete - navEntry.domInteractive,
      };
    }
    
    // 回退到旧API
    const timing = performance.timing;
    return {
   
      startTime: 0,
      domContentLoaded: timing.domContentLoadedEventEnd - timing.navigationStart,
      loadTime: timing.loadEventEnd - timing.navigationStart,
      domInteractive: timing.domInteractive - timing.navigationStart,
      redirectCount: 0,
      redirectTime: timing.redirectEnd - timing.redirectStart,
      dnsTime: timing.domainLookupEnd - timing.domainLookupStart,
      tcpTime: timing.connectEnd - timing.connectStart,
      ttfb: timing.responseStart - timing.requestStart,
      responseTime: timing.responseEnd - timing.responseStart,
      domBuildingTime: timing.domComplete - timing.domInteractive,
    };
  }
  
  // 收集并发送性能数据
  function sendPerformanceData() {
   
    const performanceData = {
   
      timestamp: Date.now(),
      page: window.location.pathname,
      referrer: document.referrer,
      deviceInfo,
      navigationTiming: collectNavigationTiming(),
      metrics: window.performanceMetrics || {
   },
      sessionId: getSessionId(),
      appVersion: config.appVersion,
      environment: config.environment
    };
    
    // 使用信标API发送数据
    if (navigator.sendBeacon) {
   
      navigator.sendBeacon(
        config.beaconEndpoint, 
        JSON.stringify(performanceData)
      );
    } else {
   
      // 回退到XHR
      const xhr = new XMLHttpRequest();
      xhr.open('POST', config.beaconEndpoint, true);
      xhr.setRequestHeader('Content-Type', 'application/json');
      xhr.send(JSON.stringify(performanceData));
    }
  }
  
  // 获取或创建会话ID
  function getSessionId() {
   
    let sessionId = sessionStorage.getItem('performance_session_id');
    if (!sessionId) {
   
      sessionId = generateUUID();
      sessionStorage.setItem('performance_session_id', sessionId);
    }
    return sessionId;
  }
  
  // 生成UUID
  function generateUUID() {
   
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
   
      const r = Math.random() * 16 | 0;
      const v = c === 'x' ? r : (r & 0x3 | 0x8);
      return v.toString(16);
    });
  }
  
  // 页面内容加载完毕后发送数据
  window.addEventListener('load', () => {
   
    // 延迟发送,等待其他指标收集完成
    setTimeout(sendPerformanceData, 1000);
  });
  
  // 页面卸载前尝试再次发送数据
  window.addEventListener('beforeunload', sendPerformanceData);
})();

性能测试工具集成

构建完整的前端性能测试体系需要集成多种专业工具,形成协同工作的工具链。

核心工具选择

不同的性能测试场景需要匹配相应的工具:

测试类型 推荐工具 主要场景
页面加载性能 Lighthouse, WebPageTest 整体页面加载分析和评分
运行时性能 Chrome DevTools, Puppeteer JavaScript执行和渲染性能分析
内存分析 Chrome Memory Panel, Heap Snapshot 内存泄漏和占用分析
真实用户监测 Google Analytics, New Relic, Sentry 生产环境中的性能数据收集
压力测试 k6, Artillery 高负载下的前端性能表现测试
工具链自动化集成

将这些工具整合到自动化测试流程中:

// 集成Lighthouse到CI/CD流程的示例脚本
const {
    exec } = require('child_process');
const lighthouse = require('lighthouse');
const chromeLauncher = require('chrome-launcher');
const fs = require('fs');
const path = require('path');

// 定义性能预算
const performanceBudgets = {
   
  'first-contentful-paint': 1800,  // ms
  'largest-contentful-paint': 2500, // ms
  'cumulative-layout-shift': 0.1,
  'total-blocking-time': 300,      // ms
  'speed-index': 3000,             // ms
  'interactive': 3500,             // ms
  'max-js-size': 350 * 1024,       // bytes
  'max-css-size': 70 * 1024,       // bytes
  'max-total-size': 1500 * 1024    // bytes
};

async function runLighthouseTest(url, options = {
    }) {
   
  // 启动Chrome
  const chrome = await chromeLauncher.launch({
   
    chromeFlags: ['--headless', '--disable-gpu', '--no-sandbox']
  });
  
  // 设置Lighthouse选项
  const opts = {
   
    port: chrome.port,
    output: 'json',
    logLevel: 'info',
    ...options
  };
  
  // 运行Lighthouse测试
  const results = await lighthouse(url, opts);
  
  // 关闭Chrome
  await chrome.kill();
  
  return results;
}

async function checkPerformanceBudgets(results, budgets) {
   
  const {
    audits } = results.lhr;
  const violations = [];
  
  // 检查FCP
  if (audits['first-contentful-paint'].numericValue > budgets['first-contentful-paint']) {
   
    violations.push({
   
      metric: 'First Contentful Paint',
      value: Math.round(audits['first-contentful-paint'].numericValue),
      budget: budgets['first-contentful-paint'],
      unit: 'ms'
    });
  }
  
  // 检查LCP
  if (audits['largest-contentful-paint'].numericValue > budgets['largest-contentful-paint']) {
   
    violations.push({
   
      metric: 'Largest Contentful Paint',
      value: Math.round(audits['largest-contentful-paint'].numericValue),
      budget: budgets['largest-contentful-paint'],
      unit: 'ms'
    });
  }
  
  // 检查CLS
  if (audits['cumulative-layout-shift'].numericValue > budgets['cumulative-layout-shift']) {
   
    violations.push({
   
      metric: 'Cumulative Layout Shift',
      value: audits['cumulative-layout-shift'].numericValue.toFixed(3),
      budget: budgets['cumulative-layout-shift'],
      unit: ''
    });
  }
  
  // 检查TBT
  if (audits['total-blocking-time'].numericValue > budgets['total-blocking-time']) {
   
    violations.push({
   
      metric: 'Total Blocking Time',
      value: Math.round(audits['total-blocking-time'].numericValue),
      budget: budgets['total-blocking-time'],
      unit: 'ms'
    });
  }
  
  // 检查Speed Index
  if (audits['speed-index'].numericValue > budgets['speed-index']) {
   
    violations.push({
   
      metric: 'Speed Index',
      value: Math.round(audits['speed-index'].numericValue),
      budget: budgets['speed-index'],
      unit: 'ms'
    });
  }
  
  // 检查TTI
  if (audits['interactive'].numericValue > budgets['interactive']) {
   
    violations.push({
   
      metric: 'Time to Interactive',
      value: Math.round(audits['interactive'].numericValue),
      budget: budgets['interactive'],
      unit: 'ms'
    });
  }
  
  // 检查JS大小
  const jsSize = audits['resource-summary'].details.items.find(item => item.resourceType === 'script').transferSize;
  if (jsSize > budgets['max-js-size']) {
   
    violations.push({
   
      metric: 'JavaScript Transfer Size',
      value: Math.round(jsSize / 1024) + ' KB',
      budget: Math.round(budgets['max-js-size'] / 1024) + ' KB',
      unit: ''
    });
  }
  
  // 检查CSS大小
  const cssSize = audits['resource-summary'].details.items.find(item => item.resourceType === 'stylesheet').transferSize;
  if (cssSize > budgets['max-css-size']) {
   
    violations.push({
   
      metric: 'CSS Transfer Size',
      value: Math.round(cssSize / 1024) + ' KB',
      budget: Math.round(budgets['max-css-size'] / 1024) + ' KB',
      unit: ''
    });
  }
  
  // 检查总资源大小
  const totalSize = audits['resource-summary'].details.items.find(item => item.resourceType === 'total').transferSize;
  if (totalSize > budgets['max-total-size']) {
   
    violations.push({
   
      metric: 'Total Transfer Size',
      value: Math.round(totalSize / 1024) + ' KB',
      budget: Math.round(budgets['max-total-size'] / 1024) + ' KB',
      unit: ''
    });
  }
  
  return violations;
}

// 主测试流程
async function runPerformanceTest() {
   
  try {
   
    console.log('启动性能测试...');
    
    // 测试环境URL
    const targetUrl = process.env.TEST_URL || 'https://staging.example.com';
    
    // 运行Lighthouse测试
    const results = await runLighthouseTest(targetUrl, {
   
      onlyCategories: ['performance']
    });
    
    // 保存测试报告
    const reportPath = path.join(__dirname, 'lighthouse-report.json');
    fs.writeFileSync(reportPath, JSON.stringify(results.lhr, null, 2));
    console.log(`测试报告已保存至: ${
     reportPath}`);
    
    // 验证性能预算
    const violations = await checkPerformanceBudgets(results, performanceBudgets);
    
    if (violations.length > 0) {
   
      console.error('性能预算违反警告:');
      violations.forEach(v => {
   
        console.error(`  - ${
     v.metric}: ${
     v.value}${
     v.unit} (预算: ${
     v.budget}${
     v.unit})`);
      })<

网站公告

今日签到

点亮在社区的每一天
去签到