前端性能优化实战指南

12 阅读6分钟

概述

性能优化是前端开发中至关重要的一环。优秀的性能不仅提升用户体验,还能提高转化率、降低跳出率,并改善 SEO 排名。本文将深入探讨前端性能优化的核心策略和实战技巧。

一、性能指标与测量

1.1 核心 Web 指标 (Core Web Vitals)

// 使用 Web Vitals 库测量核心指标
import { getCLS, getFID, getLCP } from 'web-vitals';

getCLS(console.log);   // 累积布局偏移
getFID(console.log);   // 首次输入延迟
getLCP(console.log);   // 最大内容绘制

关键指标说明:

  • LCP (Largest Contentful Paint) : 最大内容绘制,衡量加载性能

    • 优秀:≤ 2.5 秒
    • 需要改进:2.5-4.0 秒
    • 差:> 4.0 秒
  • FID (First Input Delay) : 首次输入延迟,衡量交互性

    • 优秀:≤ 100 毫秒
    • 需要改进:100-300 毫秒
    • 差:> 300 毫秒
  • CLS (Cumulative Layout Shift) : 累积布局偏移,衡量视觉稳定性

    • 优秀:≤ 0.1
    • 需要改进:0.1-0.25
    • 差:> 0.25

1.2 性能测量工具

# 使用 Lighthouse 进行性能审计
npx lighthouse https://example.com --view

# 使用 Chrome DevTools Performance 面板
# 使用 WebPageTest 进行多地点测试

# 使用 PageSpeed Insights
curl "https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=https://example.com"

二、加载性能优化

2.1 资源压缩与优化

2.1.1 图片优化

// 使用 modern 图片格式
<img src="image.webp" alt="描述" 
     srcset="image-400w.webp 400w, image-800w.webp 800w"
     sizes="(max-width: 600px) 400px, 800px"
     loading="lazy">

// 使用 picture 元素提供多种格式
<picture>
  <source srcset="image.avif" type="image/avif">
  <source srcset="image.webp" type="image/webp">
  <img src="image.jpg" alt="描述" loading="lazy">
</picture>

图片优化策略:

  • 使用 WebP/AVIF 等现代格式
  • 实现响应式图片(srcset + sizes)
  • 懒加载非首屏图片
  • 使用 CDN 进行图片优化

2.1.2 代码压缩

// Vite 配置优化
export default defineConfig({
  build: {
    minify: 'terser', // 使用 terser 进行压缩
    terserOptions: {
      compress: {
        drop_console: true, // 生产环境移除 console
        drop_debugger: true,
      },
    },
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['vue', 'vue-router', 'pinia'],
          utils: ['lodash-es', 'dayjs'],
        },
      },
    },
  },
});

2.2 资源预加载与预获取

<!-- 关键资源预加载 -->
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/js/hero.js" as="script">

<!-- 未来导航预获取 -->
<link rel="prefetch" href="/js/about.js">
<link rel="preconnect" href="https://api.example.com">

<!-- 智能预加载 -->
<script>
  // 检测用户意图,预加载可能访问的页面
  document.addEventListener('mouseover', (e) => {
    if (e.target.tagName === 'A') {
      const url = e.target.href;
      if (isSameOrigin(url)) {
        const link = document.createElement('link');
        link.rel = 'prefetch';
        link.href = url;
        document.head.appendChild(link);
      }
    }
  });
</script>

2.3 代码分割与懒加载

// 路由级代码分割
const routes = [
  {
    path: '/',
    component: () => import('@/views/Home.vue')
  },
  {
    path: '/about',
    component: () => import('@/views/About.vue')
  },
  {
    path: '/admin',
    component: () => import('@/views/Admin.vue'),
    meta: { requiresAuth: true }
  }
];

// 组件级懒加载
const HeavyChart = defineAsyncComponent({
  loader: () => import('@/components/HeavyChart.vue'),
  loadingComponent: LoadingSpinner,
  delay: 200,
  timeout: 3000
});

// 按需加载第三方库
const loadLodash = async () => {
  const _ = await import('lodash-es');
  return _.default;
};

三、运行时性能优化

3.1 渲染优化

3.1.1 虚拟列表

<!-- 实现虚拟列表处理大量数据 -->
<template>
  <div class="virtual-list" ref="listContainer">
    <div :style="{ height: totalHeight + 'px' }">
      <div 
        v-for="item in visibleItems" 
        :key="item.id"
        :style="{ 
          transform: `translateY(${item.offset}px)`,
          position: 'absolute',
          top: 0,
          left: 0,
          right: 0
        }"
      >
        <ItemComponent :item="item" />
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue';

const props = defineProps({
  items: { type: Array, required: true },
  itemHeight: { type: Number, default: 50 }
});

const listContainer = ref(null);
const scrollTop = ref(0);

const visibleCount = 20;
const totalHeight = computed(() => props.items.length * props.itemHeight);

const visibleItems = computed(() => {
  const start = Math.floor(scrollTop.value / props.itemHeight);
  const end = Math.min(start + visibleCount, props.items.length);
  return props.items
    .slice(start, end)
    .map((item, index) => ({
      ...item,
      offset: (start + index) * props.itemHeight
    }));
});

const handleScroll = () => {
  scrollTop.value = listContainer.value.scrollTop;
};

onMounted(() => {
  listContainer.value.addEventListener('scroll', handleScroll);
});

onUnmounted(() => {
  listContainer.value.removeEventListener('scroll', handleScroll);
});
</script>

3.1.2 防抖与节流

// 防抖函数
function debounce(func, wait, immediate = false) {
  let timeout;
  return function(...args) {
    const later = () => {
      timeout = null;
      if (!immediate) func.apply(this, args);
    };
    const callNow = immediate && !timeout;
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
    if (callNow) func.apply(this, args);
  };
}

// 节流函数
function throttle(func, limit) {
  let inThrottle;
  return function(...args) {
    if (!inThrottle) {
      func.apply(this, args);
      inThrottle = true;
      setTimeout(() => inThrottle = false, limit);
    }
  };
}

// 使用示例
const handleScroll = throttle(() => {
  console.log('Scroll position:', window.scrollY);
}, 100);

const handleResize = debounce(() => {
  console.log('Window resized');
  updateLayout();
}, 300);

3.2 内存优化

// 避免内存泄漏
class DataFetcher {
  constructor() {
    this.abortController = new AbortController();
    this.cache = new Map();
  }

  async fetchData(url) {
    try {
      const response = await fetch(url, {
        signal: this.abortController.signal
      });
      const data = await response.json();
      this.cache.set(url, data);
      return data;
    } catch (error) {
      if (error.name === 'AbortError') {
        console.log('Request aborted');
      } else {
        throw error;
      }
    }
  }

  destroy() {
    this.abortController.abort();
    this.cache.clear();
  }
}

// 使用 WeakMap 避免内存泄漏
const componentData = new WeakMap();

function registerComponent(component, data) {
  componentData.set(component, data);
  // 当 component 被垃圾回收时,data 也会自动释放
}

3.3 Web Worker 优化

// 主线程
const worker = new Worker('./worker.js');

worker.postMessage({
  type: 'PROCESS_DATA',
  data: largeDataSet
});

worker.onmessage = (e) => {
  const result = e.data;
  updateUI(result);
};

// worker.js
self.onmessage = (e) => {
  const { type, data } = e.data;
  
  if (type === 'PROCESS_DATA') {
    const result = heavyComputation(data);
    self.postMessage(result);
  }
};

function heavyComputation(data) {
  // 繁重的计算逻辑
  return data.map(item => item * 2).filter(x => x > 10);
}

四、网络优化

4.1 HTTP/2与HTTP/3

# Nginx HTTP/2 配置
server {
    listen 443 ssl http2;
    server_name example.com;
    
    # HTTP/2 推送
    http2_push /js/app.js;
    http2_push /css/style.css;
    
    # 多路复用优化
    tcp_nodelay on;
    tcp_nopush on;
}

4.2 缓存策略

// Service Worker 缓存策略
const CACHE_NAME = 'v1';
const CACHE_STRATEGIES = {
  // 缓存优先
  static: ['/', '/index.html', '/css/*', '/js/*'],
  
  // 网络优先
  api: '/api/*',
  
  // 过期时间
  images: {
    pattern: '/images/*',
    maxAge: 7 * 24 * 60 * 60 // 7 天
  }
};

self.addEventListener('fetch', (event) => {
  const url = new URL(event.request.url);
  
  if (CACHE_STRATEGIES.static.some(pattern => url.pathname.includes(pattern))) {
    event.respondWith(cachedFirst(event.request));
  } else if (url.pathname.startsWith('/api/')) {
    event.respondWith(networkFirst(event.request));
  } else {
    event.respondWith(staleWhileRevalidate(event.request));
  }
});

async function cachedFirst(request) {
  const cached = await caches.match(request);
  if (cached) return cached;
  
  const response = await fetch(request);
  if (response.ok) {
    const cache = await caches.open(CACHE_NAME);
    cache.put(request, response.clone());
  }
  return response;
}

4.3 请求优化

// 请求合并
class RequestBatcher {
  constructor(batchSize = 10, batchDelay = 100) {
    this.batchSize = batchSize;
    this.batchDelay = batchDelay;
    this.queue = [];
    this.timer = null;
  }

  add(request) {
    return new Promise((resolve, reject) => {
      this.queue.push({ request, resolve, reject });
      this.flush();
    });
  }

  flush() {
    if (this.timer) clearTimeout(this.timer);
    
    if (this.queue.length >= this.batchSize) {
      this.executeBatch();
    } else {
      this.timer = setTimeout(() => this.executeBatch(), this.batchDelay);
    }
  }

  async executeBatch() {
    if (this.queue.length === 0) return;
    
    const batch = [...this.queue];
    this.queue = [];
    
    try {
      const responses = await Promise.all(batch.map(item => item.request));
      batch.forEach((item, index) => item.resolve(responses[index]));
    } catch (error) {
      batch.forEach(item => item.reject(error));
    }
  }
}

// 使用示例
const batcher = new RequestBatcher();

// 批量请求
const results = await Promise.all([
  batcher.add(fetch('/api/user/1')),
  batcher.add(fetch('/api/user/2')),
  batcher.add(fetch('/api/user/3'))
]);

五、构建优化

5.1 依赖分析

# 分析打包体积
npx webpack-bundle-analyzer dist/stats.json

# 使用 source-map-explorer
npx source-map-explorer dist/js/*.js

# Vite 内置分析
vite build --analyze
// webpack 配置
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendors: {
          test: /[\/]node_modules[\/]/,
          name: 'vendors',
          chunks: 'all',
        },
        default: {
          minChunks: 2,
          priority: -10,
          reuseExistingChunk: true,
        },
      },
    },
  },
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      openAnalyzer: false,
    }),
  ],
};

5.2 Tree Shaking

// 确保使用 ES 模块语法
import { debounce, throttle } from 'lodash-es'; // ✅ 支持 tree shaking
// import _ from 'lodash'; // ❌ 会引入整个库

// 使用 sideEffects 配置
// package.json
{
  "sideEffects": [
    "*.css",
    "*.scss"
  ]
}

// 标记纯函数
/*#__PURE__*/
function pureFunction() {
  return 42;
}

六、监控与分析

6.1 性能监控

// 自定义性能监控
class PerformanceMonitor {
  constructor() {
    this.metrics = {};
    this.init();
  }

  init() {
    // 监听核心 Web 指标
    if ('PerformanceObserver' in window) {
      this.observeLCP();
      this.observeCLS();
      this.observeFID();
    }

    // 监听页面加载性能
    window.addEventListener('load', () => {
      this.recordLoadPerformance();
    });
  }

  observeLCP() {
    new PerformanceObserver((list) => {
      const entries = list.getEntries();
      const lastEntry = entries[entries.length - 1];
      this.recordMetric('LCP', lastEntry.startTime);
    }).observe({ type: 'largest-contentful-paint', buffered: true });
  }

  observeCLS() {
    let clsValue = 0;
    new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (!entry.hadRecentInput) {
          clsValue += entry.value;
        }
      }
      this.recordMetric('CLS', clsValue);
    }).observe({ type: 'layout-shift', buffered: true });
  }

  recordMetric(name, value) {
    this.metrics[name] = value;
    
    // 发送到分析服务
    this.sendToAnalytics(name, value);
  }

  recordLoadPerformance() {
    const timing = performance.timing;
    const loadTime = timing.loadEventEnd - timing.navigationStart;
    this.recordMetric('LoadTime', loadTime);
  }

  sendToAnalytics(name, value) {
    // 发送到监控服务
    navigator.sendBeacon('/api/performance', 
      JSON.stringify({ metric: name, value, timestamp: Date.now() })
    );
  }

  getReport() {
    return {
      ...this.metrics,
      timestamp: new Date().toISOString(),
      userAgent: navigator.userAgent,
      url: window.location.href
    };
  }
}

// 使用
const monitor = new PerformanceMonitor();

6.2 错误监控

// 全局错误处理
window.addEventListener('error', (event) => {
  reportError({
    type: 'javascript',
    message: event.message,
    filename: event.filename,
    lineno: event.lineno,
    colno: event.colno,
    error: event.error
  });
});

window.addEventListener('unhandledrejection', (event) => {
  reportError({
    type: 'promise',
    message: event.reason?.message || 'Unhandled promise rejection',
    error: event.reason
  });
});

function reportError(error) {
  // 发送到错误监控服务
  navigator.sendBeacon('/api/error', JSON.stringify({
    ...error,
    timestamp: Date.now(),
    url: window.location.href,
    userAgent: navigator.userAgent
  }));
  
  // 可选:记录到控制台
  console.error('Performance Error:', error);
}

七、实战案例

7.1 电商网站性能优化

优化前:

  • LCP: 4.2s
  • FID: 350ms
  • CLS: 0.35
  • 首屏加载时间:5.1s

优化措施:

  1. 图片优化(WebP + 懒加载)
  2. 代码分割(路由级 + 组件级)
  3. 预加载关键资源
  4. Service Worker 缓存
  5. HTTP/2 启用

优化后:

  • LCP: 1.8s ✅
  • FID: 85ms ✅
  • CLS: 0.08 ✅
  • 首屏加载时间:2.1s ✅

总结

前端性能优化是一个持续的过程,需要:

核心策略:

  1. 测量先行 - 使用工具了解当前性能状况
  2. 渐进优化 - 从影响最大的地方开始
  3. 持续监控 - 建立性能监控体系
  4. 团队协作 - 将性能纳入开发流程

关键要点:

  • 图片优化通常带来最大收益
  • 代码分割能显著改善首屏加载
  • 缓存策略对重复访问至关重要
  • 运行时优化提升用户体验
  • 监控确保优化效果持续

记住:性能优化不是一次性的任务,而是持续改进的过程。定期测量、分析、优化,确保你的应用始终保持最佳性能。