每天一个高级前端知识 - Day 16

1 阅读4分钟

每天一个高级前端知识 - Day 16

今日主题:性能监控与优化 - 从用户体验到Core Web Vitals的完整实践

核心概念:性能不是优化的结果,而是监控的起点

无法衡量的性能无法优化。建立完整的性能监控体系,是持续优化用户体验的基础。

📊 Core Web Vitals 深度解析

Core Web Vitals (Google排名因子)
├── LCP (Largest Contentful Paint) - 加载性能
│   └── 目标: ≤ 2.5秒
├── INP (Interaction to Next Paint) - 交互响应
│   └── 目标: ≤ 200毫秒 (替代FID)
└── CLS (Cumulative Layout Shift) - 视觉稳定
    └── 目标: ≤ 0.1

🔍 完整性能监控系统

// 1. 性能监控类
class PerformanceMonitor {
  constructor(config = {}) {
    this.config = {
      sampleRate: 0.1, // 10%采样
      endpoint: '/api/performance',
      appId: 'your-app-id',
      ...config
    };
    
    this.metrics = [];
    this.observers = [];
  }
  
  // 初始化所有监控
  init() {
    // 采样控制
    if (Math.random() > this.config.sampleRate) return;
    
    this.observeLCP();
    this.observeINP();
    this.observeCLS();
    this.observeFCP();
    this.observeTTFB();
    this.observeLongTasks();
    this.observeMemory();
    this.observeNetwork();
    this.observeResourceTiming();
    
    // 页面关闭时上报
    window.addEventListener('beforeunload', () => this.flush());
    window.addEventListener('pagehide', () => this.flush());
    
    // 定期上报
    setInterval(() => this.flush(), 30000);
  }
  
  // LCP监控
  observeLCP() {
    const observer = new PerformanceObserver((list) => {
      const entries = list.getEntries();
      const lastEntry = entries[entries.length - 1];
      
      this.metrics.push({
        name: 'LCP',
        value: lastEntry.renderTime || lastEntry.loadTime,
        rating: this.getRating('LCP', lastEntry.renderTime || lastEntry.loadTime),
        url: lastEntry.url,
        element: lastEntry.element?.tagName,
        timestamp: Date.now()
      });
    });
    
    observer.observe({ type: 'largest-contentful-paint', buffered: true });
    this.observers.push(observer);
  }
  
  // INP监控 (Interaction to Next Paint)
  observeINP() {
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (entry.interactionId) {
          this.metrics.push({
            name: 'INP',
            value: entry.processingEnd - entry.startTime,
            rating: this.getRating('INP', entry.processingEnd - entry.startTime),
            interactionType: entry.name,
            target: entry.target?.id || entry.target?.className,
            timestamp: Date.now()
          });
        }
      }
    });
    
    observer.observe({ type: 'event', buffered: true, durationThreshold: 16 });
    this.observers.push(observer);
  }
  
  // CLS监控
  observeCLS() {
    let clsValue = 0;
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (!entry.hadRecentInput) {
          clsValue += entry.value;
          
          this.metrics.push({
            name: 'CLS',
            value: clsValue,
            rating: this.getRating('CLS', clsValue),
            sources: entry.sources?.map(s => ({
              node: s.node?.tagName,
              previousRect: s.previousRect,
              currentRect: s.currentRect
            })),
            timestamp: Date.now()
          });
        }
      }
    });
    
    observer.observe({ type: 'layout-shift', buffered: true });
    this.observers.push(observer);
  }
  
  // FCP监控 (First Contentful Paint)
  observeFCP() {
    const observer = new PerformanceObserver((list) => {
      const entries = list.getEntries();
      const fcp = entries.find(entry => entry.name === 'first-contentful-paint');
      
      if (fcp) {
        this.metrics.push({
          name: 'FCP',
          value: fcp.startTime,
          rating: this.getRating('FCP', fcp.startTime),
          timestamp: Date.now()
        });
      }
    });
    
    observer.observe({ type: 'paint', buffered: true });
    this.observers.push(observer);
  }
  
  // TTFB监控 (Time To First Byte)
  observeTTFB() {
    const observer = new PerformanceObserver((list) => {
      const entries = list.getEntries();
      const navigation = entries.find(e => e.entryType === 'navigation');
      
      if (navigation) {
        this.metrics.push({
          name: 'TTFB',
          value: navigation.responseStart - navigation.requestStart,
          rating: this.getRating('TTFB', navigation.responseStart - navigation.requestStart),
          timestamp: Date.now()
        });
      }
    });
    
    observer.observe({ type: 'navigation', buffered: true });
    this.observers.push(observer);
  }
  
  // 长任务监控
  observeLongTasks() {
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (entry.duration > 50) {
          this.metrics.push({
            name: 'LongTask',
            value: entry.duration,
            startTime: entry.startTime,
            attribution: entry.attribution?.[0]?.containerType,
            scriptUrl: entry.attribution?.[0]?.scriptUrl,
            timestamp: Date.now()
          });
          
          // 发送告警
          this.alertLongTask(entry);
        }
      }
    });
    
    observer.observe({ type: 'longtask', buffered: true });
    this.observers.push(observer);
  }
  
  // 内存监控
  observeMemory() {
    if (performance.memory) {
      setInterval(() => {
        const memory = performance.memory;
        this.metrics.push({
          name: 'Memory',
          usedJSHeapSize: memory.usedJSHeapSize,
          totalJSHeapSize: memory.totalJSHeapSize,
          jsHeapSizeLimit: memory.jsHeapSizeLimit,
          usage: (memory.usedJSHeapSize / memory.jsHeapSizeLimit) * 100,
          timestamp: Date.now()
        });
        
        // 内存告警
        if (memory.usedJSHeapSize / memory.jsHeapSizeLimit > 0.8) {
          this.alertHighMemory();
        }
      }, 60000);
    }
  }
  
  // 网络监控
  observeNetwork() {
    if ('connection' in navigator) {
      const connection = navigator.connection;
      
      const updateNetworkInfo = () => {
        this.metrics.push({
          name: 'NetworkInfo',
          effectiveType: connection.effectiveType,
          rtt: connection.rtt,
          downlink: connection.downlink,
          saveData: connection.saveData,
          timestamp: Date.now()
        });
      };
      
      updateNetworkInfo();
      connection.addEventListener('change', updateNetworkInfo);
    }
  }
  
  // 资源加载监控
  observeResourceTiming() {
    const observer = new PerformanceObserver((list) => {
      const slowResources = list.getEntries().filter(entry => 
        entry.duration > 1000 || entry.transferSize > 1000000
      );
      
      for (const resource of slowResources) {
        this.metrics.push({
          name: 'SlowResource',
          url: resource.name,
          duration: resource.duration,
          size: resource.transferSize,
          type: resource.initiatorType,
          timestamp: Date.now()
        });
      }
    });
    
    observer.observe({ type: 'resource', buffered: true });
    this.observers.push(observer);
  }
  
  // 自定义指标
  markCustomMetric(name, value) {
    performance.mark(`${name}-start`);
    // 执行操作
    performance.mark(`${name}-end`);
    performance.measure(name, `${name}-start`, `${name}-end`);
    
    const measure = performance.getEntriesByName(name)[0];
    this.metrics.push({
      name: `Custom_${name}`,
      value: measure.duration,
      customValue: value,
      timestamp: Date.now()
    });
  }
  
  // 评分规则
  getRating(metric, value) {
    const thresholds = {
      LCP: { good: 2500, needImprovement: 4000 },
      INP: { good: 200, needImprovement: 500 },
      CLS: { good: 0.1, needImprovement: 0.25 },
      FCP: { good: 1800, needImprovement: 3000 },
      TTFB: { good: 800, needImprovement: 1800 }
    };
    
    const threshold = thresholds[metric];
    if (!threshold) return 'unknown';
    
    if (value <= threshold.good) return 'good';
    if (value <= threshold.needImprovement) return 'needs-improvement';
    return 'poor';
  }
  
  // 性能评分
  calculatePerformanceScore() {
    const metrics = this.metrics.filter(m => 
      ['LCP', 'INP', 'CLS', 'FCP', 'TTFB'].includes(m.name)
    );
    
    const latest = {};
    for (const metric of metrics) {
      if (!latest[metric.name] || metric.timestamp > latest[metric.name].timestamp) {
        latest[metric.name] = metric;
      }
    }
    
    const weights = { LCP: 0.25, INP: 0.25, CLS: 0.15, FCP: 0.2, TTFB: 0.15 };
    let score = 0;
    
    for (const [name, weight] of Object.entries(weights)) {
      const metric = latest[name];
      if (metric) {
        const ratingScore = { good: 1, 'needs-improvement': 0.5, poor: 0 };
        score += ratingScore[metric.rating] * weight;
      }
    }
    
    return Math.round(score * 100);
  }
  
  // 告警
  alertLongTask(task) {
    if (task.duration > 200) {
      this.sendAlert({
        type: 'LONG_TASK',
        severity: 'high',
        message: `检测到长任务 ${task.duration}ms`,
        data: task
      });
    }
  }
  
  alertHighMemory() {
    this.sendAlert({
      type: 'HIGH_MEMORY',
      severity: 'medium',
      message: '内存使用超过80%',
      data: performance.memory
    });
  }
  
  sendAlert(alert) {
    if (navigator.sendBeacon) {
      navigator.sendBeacon('/api/alerts', JSON.stringify(alert));
    }
  }
  
  // 上报数据
  flush() {
    if (this.metrics.length === 0) return;
    
    const payload = {
      appId: this.config.appId,
      url: window.location.href,
      userAgent: navigator.userAgent,
      timestamp: Date.now(),
      metrics: this.metrics,
      score: this.calculatePerformanceScore()
    };
    
    // 使用sendBeacon确保页面关闭时也能发送
    if (navigator.sendBeacon) {
      navigator.sendBeacon(this.config.endpoint, JSON.stringify(payload));
    } else {
      fetch(this.config.endpoint, {
        method: 'POST',
        body: JSON.stringify(payload),
        keepalive: true
      });
    }
    
    this.metrics = [];
  }
  
  // 断开所有观察者
  disconnect() {
    this.observers.forEach(observer => observer.disconnect());
    this.observers = [];
  }
}

// 使用
const monitor = new PerformanceMonitor({
  sampleRate: 0.5,
  endpoint: 'https://analytics.company.com/api/vitals',
  appId: 'production-web'
});

monitor.init();

// 自定义业务指标
monitor.markCustomMetric('search-response', searchTime);
monitor.markCustomMetric('cart-load', cartLoadTime);

🚀 性能优化实战

// 1. 图片优化策略
class ImageOptimizer {
  // 响应式图片
  static getResponsiveSrc(src, width) {
    return `${src}?w=${width}&q=80&format=auto`;
  }
  
  // 懒加载
  static lazyLoad() {
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const img = entry.target;
          img.src = img.dataset.src;
          img.srcset = img.dataset.srcset;
          observer.unobserve(img);
        }
      });
    });
    
    document.querySelectorAll('img[data-src]').forEach(img => {
      observer.observe(img);
    });
  }
  
  // WebP支持检测
  static async supportsWebP() {
    const canvas = document.createElement('canvas');
    canvas.width = 1;
    canvas.height = 1;
    const ctx = canvas.getContext('2d');
    const data = ctx.getImageData(0, 0, 1, 1);
    return canvas.toDataURL('image/webp').indexOf('image/webp') === 5;
  }
  
  // 使用现代格式
  static async getOptimalFormat() {
    const supports = {
      webp: await this.supportsWebP(),
      avif: await this.supportsAVIF()
    };
    
    if (supports.avif) return 'avif';
    if (supports.webp) return 'webp';
    return 'jpeg';
  }
}

// 2. 代码分割与动态导入
const routes = {
  dashboard: () => import(/* webpackChunkName: "dashboard" */ './routes/Dashboard'),
  profile: () => import(/* webpackChunkName: "profile" */ './routes/Profile'),
  settings: () => import(/* webpackChunkName: "settings" */ './routes/Settings')
};

// 预加载策略
function preloadRoute(routeName) {
  if (requestIdleCallback) {
    requestIdleCallback(() => {
      routes[routeName]();
    });
  }
}

// 3. 资源提示
function addResourceHints() {
  // DNS预解析
  const dnsPrefetch = document.createElement('link');
  dnsPrefetch.rel = 'dns-prefetch';
  dnsPrefetch.href = 'https://api.example.com';
  document.head.appendChild(dnsPrefetch);
  
  // 预连接
  const preconnect = document.createElement('link');
  preconnect.rel = 'preconnect';
  preconnect.href = 'https://cdn.example.com';
  preconnect.crossOrigin = 'anonymous';
  document.head.appendChild(preconnect);
  
  // 预加载关键资源
  const preload = document.createElement('link');
  preload.rel = 'preload';
  preload.as = 'font';
  preload.href = '/fonts/main.woff2';
  preload.crossOrigin = 'anonymous';
  document.head.appendChild(preload);
}

// 4. 虚拟滚动(处理大数据列表)
class VirtualScroller {
  constructor(container, options) {
    this.container = container;
    this.itemHeight = options.itemHeight;
    this.renderItem = options.renderItem;
    this.totalItems = options.totalItems;
    this.bufferSize = options.bufferSize || 5;
    
    this.scrollTop = 0;
    this.startIndex = 0;
    this.endIndex = 0;
    
    this.setupDOM();
    this.bindEvents();
    this.render();
  }
  
  setupDOM() {
    this.viewport = document.createElement('div');
    this.viewport.style.height = '100%';
    this.viewport.style.overflow = 'auto';
    this.viewport.style.position = 'relative';
    
    this.content = document.createElement('div');
    this.content.style.position = 'absolute';
    this.content.style.top = '0';
    this.content.style.left = '0';
    this.content.style.width = '100%';
    
    this.container.appendChild(this.viewport);
    this.viewport.appendChild(this.content);
    
    // 设置总高度
    this.viewport.style.height = `${this.container.clientHeight}px`;
    this.content.style.height = `${this.totalItems * this.itemHeight}px`;
  }
  
  bindEvents() {
    this.viewport.addEventListener('scroll', () => {
      this.scrollTop = this.viewport.scrollTop;
      this.updateVisibleRange();
    });
    
    window.addEventListener('resize', () => {
      this.viewport.style.height = `${this.container.clientHeight}px`;
      this.updateVisibleRange();
    });
  }
  
  updateVisibleRange() {
    const viewportHeight = this.viewport.clientHeight;
    const start = Math.floor(this.scrollTop / this.itemHeight);
    const visibleCount = Math.ceil(viewportHeight / this.itemHeight);
    
    this.startIndex = Math.max(0, start - this.bufferSize);
    this.endIndex = Math.min(
      this.totalItems - 1,
      start + visibleCount + this.bufferSize
    );
    
    this.render();
  }
  
  render() {
    const fragment = document.createDocumentFragment();
    
    for (let i = this.startIndex; i <= this.endIndex; i++) {
      const item = this.renderItem(i);
      item.style.position = 'absolute';
      item.style.top = `${i * this.itemHeight}px`;
      item.style.left = '0';
      item.style.width = '100%';
      item.style.height = `${this.itemHeight}px`;
      fragment.appendChild(item);
    }
    
    this.content.innerHTML = '';
    this.content.appendChild(fragment);
  }
}

📈 性能预算

// performance-budget.js
const budget = {
  // 加载预算
  loading: {
    LCP: 2500,
    TTFB: 800,
    FCP: 1800,
    totalSize: 500000, // 500KB
    jsSize: 200000,    // 200KB
    cssSize: 50000,    // 50KB
    imageSize: 250000  // 250KB
  },
  
  // 交互预算
  interactivity: {
    INP: 200,
    TTI: 5000,
    FID: 100
  },
  
  // 视觉稳定
  visual: {
    CLS: 0.1
  }
};

// 构建时检查
class PerformanceBudgetChecker {
  constructor(budget) {
    this.budget = budget;
  }
  
  async checkBundle() {
    const stats = await this.getBundleStats();
    const violations = [];
    
    if (stats.js > this.budget.loading.jsSize) {
      violations.push(`JS大小超限: ${stats.js} > ${this.budget.loading.jsSize}`);
    }
    
    if (stats.css > this.budget.loading.cssSize) {
      violations.push(`CSS大小超限: ${stats.css} > ${this.budget.loading.cssSize}`);
    }
    
    if (violations.length > 0) {
      console.error('性能预算违规:', violations);
      if (process.env.NODE_ENV === 'production') {
        process.exit(1);
      }
    }
    
    return violations;
  }
}

🎯 今日挑战

实现一个完整的性能监控面板,要求:

  1. 实时显示Core Web Vitals (LCP, INP, CLS)
  2. 显示资源加载瀑布图
  3. 显示长任务列表和堆栈
  4. 提供优化建议(基于Lighthouse规则)
  5. 导出性能报告(PDF/JSON)
  6. 对比多个版本的性能变化
// 使用示例
const dashboard = new PerformanceDashboard({
  target: document.getElementById('dashboard'),
  autoRefresh: true,
  refreshInterval: 5000,
  historyLength: 100
});

dashboard.start();
dashboard.on('metric-update', (metrics) => {
  console.log('新指标:', metrics);
});
dashboard.exportReport('pdf');

明日预告:前端安全 - XSS、CSRF、点击劫持等攻击的防御实战

💡 性能箴言:"用户不会抱怨快,只会抱怨慢"——把性能目标设为"感觉不到等待"!