Skip to content

前端性能优化实战指南

前端性能优化是提升用户体验的关键因素。本文将从多个维度深入探讨前端性能优化的策略和实践,帮助你构建更快、更流畅的 Web 应用。

1. 性能指标与测量

1.1 Core Web Vitals

javascript
// 测量 Core Web Vitals
function measureWebVitals() {
  // Largest Contentful Paint (LCP)
  new PerformanceObserver((entryList) => {
    const entries = entryList.getEntries()
    const lastEntry = entries[entries.length - 1]
    console.log('LCP:', lastEntry.startTime)
  }).observe({ entryTypes: ['largest-contentful-paint'] })
  
  // First Input Delay (FID)
  new PerformanceObserver((entryList) => {
    const entries = entryList.getEntries()
    entries.forEach((entry) => {
      if (entry.name === 'first-input') {
        const fid = entry.processingStart - entry.startTime
        console.log('FID:', fid)
      }
    })
  }).observe({ entryTypes: ['first-input'] })
  
  // Cumulative Layout Shift (CLS)
  let clsValue = 0
  new PerformanceObserver((entryList) => {
    const entries = entryList.getEntries()
    entries.forEach((entry) => {
      if (!entry.hadRecentInput) {
        clsValue += entry.value
        console.log('CLS:', clsValue)
      }
    })
  }).observe({ entryTypes: ['layout-shift'] })
}

// 页面加载完成后测量
window.addEventListener('load', measureWebVitals)

1.2 自定义性能指标

javascript
// 自定义性能测量
class PerformanceTracker {
  constructor() {
    this.marks = new Map()
    this.measures = new Map()
  }
  
  mark(name) {
    const timestamp = performance.now()
    this.marks.set(name, timestamp)
    performance.mark(name)
  }
  
  measure(name, startMark, endMark) {
    const startTime = this.marks.get(startMark)
    const endTime = this.marks.get(endMark)
    
    if (startTime && endTime) {
      const duration = endTime - startTime
      this.measures.set(name, duration)
      performance.measure(name, startMark, endMark)
      return duration
    }
  }
  
  getMetrics() {
    return {
      marks: Object.fromEntries(this.marks),
      measures: Object.fromEntries(this.measures)
    }
  }
}

const tracker = new PerformanceTracker()

// 使用示例
tracker.mark('api-start')
fetch('/api/data')
  .then(response => {
    tracker.mark('api-end')
    tracker.measure('api-duration', 'api-start', 'api-end')
    return response.json()
  })

2. 加载性能优化

2.1 资源优化

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

<!-- 预连接外部域名 -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://api.example.com">

<!-- DNS 预解析 -->
<link rel="dns-prefetch" href="//cdn.example.com">

<!-- 预获取下一页资源 -->
<link rel="prefetch" href="/page2.html">
<link rel="prefetch" href="/api/next-data">

2.2 图片优化

html
<!-- 响应式图片 -->
<picture>
  <source media="(min-width: 800px)" srcset="large.webp" type="image/webp">
  <source media="(min-width: 800px)" srcset="large.jpg">
  <source media="(min-width: 400px)" srcset="medium.webp" type="image/webp">
  <source media="(min-width: 400px)" srcset="medium.jpg">
  <img src="small.jpg" alt="描述" loading="lazy">
</picture>

<!-- 现代图片格式 -->
<img src="image.jpg" 
     srcset="image-320w.webp 320w, image-640w.webp 640w, image-1280w.webp 1280w"
     sizes="(max-width: 320px) 280px, (max-width: 640px) 600px, 1200px"
     alt="描述"
     loading="lazy">
javascript
// 图片懒加载实现
class LazyImageLoader {
  constructor() {
    this.imageObserver = new IntersectionObserver(
      this.handleIntersection.bind(this),
      {
        rootMargin: '50px 0px',
        threshold: 0.01
      }
    )
    
    this.init()
  }
  
  init() {
    const lazyImages = document.querySelectorAll('img[data-src]')
    lazyImages.forEach(img => this.imageObserver.observe(img))
  }
  
  handleIntersection(entries) {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const img = entry.target
        this.loadImage(img)
        this.imageObserver.unobserve(img)
      }
    })
  }
  
  loadImage(img) {
    const src = img.dataset.src
    const srcset = img.dataset.srcset
    
    if (src) {
      img.src = src
    }
    
    if (srcset) {
      img.srcset = srcset
    }
    
    img.classList.add('loaded')
    
    // 移除 data 属性
    delete img.dataset.src
    delete img.dataset.srcset
  }
}

new LazyImageLoader()

2.3 代码分割

javascript
// 动态导入
const loadModule = async (moduleName) => {
  try {
    const module = await import(`./modules/${moduleName}.js`)
    return module.default
  } catch (error) {
    console.error(`Failed to load module: ${moduleName}`, error)
    throw error
  }
}

// 路由级别的代码分割
const routes = [
  {
    path: '/',
    component: () => import('./views/Home.vue')
  },
  {
    path: '/dashboard',
    component: () => import('./views/Dashboard.vue')
  },
  {
    path: '/admin',
    component: () => import('./views/Admin.vue')
  }
]

// 组件级别的代码分割
const AsyncComponent = defineAsyncComponent({
  loader: () => import('./HeavyComponent.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorComponent,
  delay: 200,
  timeout: 3000
})

3. 运行时性能优化

3.1 JavaScript 优化

javascript
// 防抖和节流
function debounce(func, wait, immediate) {
  let timeout
  return function executedFunction(...args) {
    const later = () => {
      timeout = null
      if (!immediate) func(...args)
    }
    const callNow = immediate && !timeout
    clearTimeout(timeout)
    timeout = setTimeout(later, wait)
    if (callNow) func(...args)
  }
}

function throttle(func, limit) {
  let inThrottle
  return function(...args) {
    if (!inThrottle) {
      func.apply(this, args)
      inThrottle = true
      setTimeout(() => inThrottle = false, limit)
    }
  }
}

// 使用示例
const debouncedSearch = debounce((query) => {
  searchAPI(query)
}, 300)

const throttledScroll = throttle(() => {
  updateScrollPosition()
}, 16) // 60fps

// 事件监听
input.addEventListener('input', debouncedSearch)
window.addEventListener('scroll', throttledScroll)

3.2 DOM 操作优化

javascript
// 批量 DOM 操作
function batchDOMUpdates(updates) {
  const fragment = document.createDocumentFragment()
  
  updates.forEach(update => {
    const element = document.createElement(update.tag)
    element.textContent = update.text
    element.className = update.className
    fragment.appendChild(element)
  })
  
  // 一次性插入到 DOM
  document.getElementById('container').appendChild(fragment)
}

// 使用 requestAnimationFrame 优化动画
function smoothScroll(element, to, duration) {
  const start = element.scrollTop
  const change = to - start
  const startTime = performance.now()
  
  function animateScroll(currentTime) {
    const timeElapsed = currentTime - startTime
    const progress = Math.min(timeElapsed / duration, 1)
    
    element.scrollTop = start + change * easeInOutQuad(progress)
    
    if (progress < 1) {
      requestAnimationFrame(animateScroll)
    }
  }
  
  requestAnimationFrame(animateScroll)
}

function easeInOutQuad(t) {
  return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t
}

3.3 内存管理

javascript
// 内存泄漏预防
class ComponentManager {
  constructor() {
    this.eventListeners = new Map()
    this.timers = new Set()
    this.observers = new Set()
  }
  
  addEventListener(element, event, handler) {
    element.addEventListener(event, handler)
    
    if (!this.eventListeners.has(element)) {
      this.eventListeners.set(element, [])
    }
    this.eventListeners.get(element).push({ event, handler })
  }
  
  setTimeout(callback, delay) {
    const timerId = setTimeout(callback, delay)
    this.timers.add(timerId)
    return timerId
  }
  
  setInterval(callback, interval) {
    const timerId = setInterval(callback, interval)
    this.timers.add(timerId)
    return timerId
  }
  
  observe(observer) {
    this.observers.add(observer)
    return observer
  }
  
  cleanup() {
    // 清理事件监听器
    this.eventListeners.forEach((listeners, element) => {
      listeners.forEach(({ event, handler }) => {
        element.removeEventListener(event, handler)
      })
    })
    this.eventListeners.clear()
    
    // 清理定时器
    this.timers.forEach(timerId => {
      clearTimeout(timerId)
      clearInterval(timerId)
    })
    this.timers.clear()
    
    // 清理观察者
    this.observers.forEach(observer => {
      if (observer.disconnect) {
        observer.disconnect()
      }
    })
    this.observers.clear()
  }
}

4. 缓存策略

4.1 HTTP 缓存

javascript
// Service Worker 缓存策略
self.addEventListener('fetch', event => {
  const { request } = event
  const url = new URL(request.url)
  
  // 静态资源缓存优先
  if (request.destination === 'script' || 
      request.destination === 'style' || 
      request.destination === 'image') {
    event.respondWith(
      caches.match(request).then(response => {
        return response || fetch(request).then(fetchResponse => {
          const responseClone = fetchResponse.clone()
          caches.open('static-v1').then(cache => {
            cache.put(request, responseClone)
          })
          return fetchResponse
        })
      })
    )
  }
  
  // API 请求网络优先
  if (url.pathname.startsWith('/api/')) {
    event.respondWith(
      fetch(request).then(response => {
        const responseClone = response.clone()
        caches.open('api-v1').then(cache => {
          cache.put(request, responseClone)
        })
        return response
      }).catch(() => {
        return caches.match(request)
      })
    )
  }
})

4.2 应用级缓存

javascript
// LRU 缓存实现
class LRUCache {
  constructor(capacity) {
    this.capacity = capacity
    this.cache = new Map()
  }
  
  get(key) {
    if (this.cache.has(key)) {
      const value = this.cache.get(key)
      // 移动到最新位置
      this.cache.delete(key)
      this.cache.set(key, value)
      return value
    }
    return null
  }
  
  set(key, value) {
    if (this.cache.has(key)) {
      this.cache.delete(key)
    } else if (this.cache.size >= this.capacity) {
      // 删除最久未使用的项
      const firstKey = this.cache.keys().next().value
      this.cache.delete(firstKey)
    }
    this.cache.set(key, value)
  }
  
  clear() {
    this.cache.clear()
  }
}

// 使用示例
const apiCache = new LRUCache(100)

async function fetchWithCache(url) {
  const cached = apiCache.get(url)
  if (cached) {
    return cached
  }
  
  const response = await fetch(url)
  const data = await response.json()
  apiCache.set(url, data)
  return data
}

5. 网络优化

5.1 HTTP/2 优化

javascript
// 服务器推送
app.get('/', (req, res) => {
  // 推送关键资源
  res.push('/css/critical.css')
  res.push('/js/main.js')
  res.push('/fonts/main.woff2')
  
  res.render('index')
})

// 多路复用优化
const fetchMultiple = async (urls) => {
  const promises = urls.map(url => fetch(url))
  const responses = await Promise.all(promises)
  return Promise.all(responses.map(res => res.json()))
}

5.2 请求优化

javascript
// 请求合并
class RequestBatcher {
  constructor(batchSize = 10, delay = 100) {
    this.batchSize = batchSize
    this.delay = delay
    this.queue = []
    this.timer = null
  }
  
  add(request) {
    return new Promise((resolve, reject) => {
      this.queue.push({ request, resolve, reject })
      
      if (this.queue.length >= this.batchSize) {
        this.flush()
      } else if (!this.timer) {
        this.timer = setTimeout(() => this.flush(), this.delay)
      }
    })
  }
  
  async flush() {
    if (this.timer) {
      clearTimeout(this.timer)
      this.timer = null
    }
    
    const batch = this.queue.splice(0, this.batchSize)
    if (batch.length === 0) return
    
    try {
      const requests = batch.map(item => item.request)
      const response = await fetch('/api/batch', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ requests })
      })
      
      const results = await response.json()
      
      batch.forEach((item, index) => {
        item.resolve(results[index])
      })
    } catch (error) {
      batch.forEach(item => item.reject(error))
    }
  }
}

const batcher = new RequestBatcher()

// 使用示例
const getUserData = (userId) => {
  return batcher.add({ type: 'getUser', userId })
}

6. 构建优化

6.1 Webpack 优化

javascript
// webpack.config.js
const path = require('path')
const TerserPlugin = require('terser-webpack-plugin')
const CompressionPlugin = require('compression-webpack-plugin')

module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
        },
        common: {
          name: 'common',
          minChunks: 2,
          chunks: 'all',
          enforce: true
        }
      }
    },
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: {
            drop_console: true,
            drop_debugger: true
          }
        }
      })
    ]
  },
  
  plugins: [
    new CompressionPlugin({
      algorithm: 'gzip',
      test: /\.(js|css|html|svg)$/,
      threshold: 8192,
      minRatio: 0.8
    })
  ],
  
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
  }
}

6.2 Vite 优化

javascript
// vite.config.js
import { defineConfig } from 'vite'
import { resolve } from 'path'

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['vue', 'vue-router'],
          ui: ['element-plus'],
          utils: ['lodash', 'dayjs']
        }
      }
    },
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true
      }
    }
  },
  
  optimizeDeps: {
    include: ['vue', 'vue-router'],
    exclude: ['@vueuse/core']
  }
})

7. 监控与分析

7.1 性能监控

javascript
// 性能监控系统
class PerformanceMonitor {
  constructor() {
    this.metrics = {}
    this.observers = []
    this.init()
  }
  
  init() {
    this.observeNavigation()
    this.observeResources()
    this.observeLongTasks()
    this.observeMemory()
  }
  
  observeNavigation() {
    window.addEventListener('load', () => {
      const navigation = performance.getEntriesByType('navigation')[0]
      this.metrics.navigation = {
        dns: navigation.domainLookupEnd - navigation.domainLookupStart,
        tcp: navigation.connectEnd - navigation.connectStart,
        request: navigation.responseStart - navigation.requestStart,
        response: navigation.responseEnd - navigation.responseStart,
        dom: navigation.domContentLoadedEventEnd - navigation.navigationStart,
        load: navigation.loadEventEnd - navigation.navigationStart
      }
    })
  }
  
  observeResources() {
    const observer = new PerformanceObserver((list) => {
      const entries = list.getEntries()
      entries.forEach(entry => {
        if (entry.duration > 1000) { // 超过1秒的资源
          console.warn('Slow resource:', entry.name, entry.duration)
        }
      })
    })
    
    observer.observe({ entryTypes: ['resource'] })
    this.observers.push(observer)
  }
  
  observeLongTasks() {
    const observer = new PerformanceObserver((list) => {
      const entries = list.getEntries()
      entries.forEach(entry => {
        console.warn('Long task detected:', entry.duration)
      })
    })
    
    observer.observe({ entryTypes: ['longtask'] })
    this.observers.push(observer)
  }
  
  observeMemory() {
    if ('memory' in performance) {
      setInterval(() => {
        const memory = performance.memory
        this.metrics.memory = {
          used: memory.usedJSHeapSize,
          total: memory.totalJSHeapSize,
          limit: memory.jsHeapSizeLimit
        }
        
        // 内存使用率超过80%时警告
        if (memory.usedJSHeapSize / memory.jsHeapSizeLimit > 0.8) {
          console.warn('High memory usage detected')
        }
      }, 10000) // 每10秒检查一次
    }
  }
  
  getMetrics() {
    return this.metrics
  }
  
  sendMetrics() {
    fetch('/api/metrics', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(this.metrics)
    })
  }
}

const monitor = new PerformanceMonitor()

8. 最佳实践总结

8.1 加载优化清单

  • [ ] 启用 HTTP/2
  • [ ] 使用 CDN
  • [ ] 压缩资源(Gzip/Brotli)
  • [ ] 优化图片格式和大小
  • [ ] 实现懒加载
  • [ ] 代码分割
  • [ ] 预加载关键资源
  • [ ] 移除未使用的代码

8.2 运行时优化清单

  • [ ] 防抖和节流
  • [ ] 虚拟滚动
  • [ ] 内存泄漏预防
  • [ ] 避免强制同步布局
  • [ ] 优化动画性能
  • [ ] 合理使用缓存
  • [ ] 减少 DOM 操作
  • [ ] 优化事件处理

8.3 监控清单

  • [ ] Core Web Vitals 监控
  • [ ] 资源加载监控
  • [ ] 错误监控
  • [ ] 用户行为分析
  • [ ] 性能预算设置
  • [ ] 持续性能测试

总结

前端性能优化是一个持续的过程,需要从多个维度进行考虑:

  1. 测量为先:建立完善的性能监控体系
  2. 加载优化:减少资源大小,优化加载策略
  3. 运行时优化:提升代码执行效率
  4. 缓存策略:合理利用各级缓存
  5. 网络优化:减少网络请求,提升传输效率
  6. 构建优化:优化打包和部署流程
  7. 持续监控:建立性能监控和告警机制

通过系统性的优化策略,可以显著提升 Web 应用的性能和用户体验。

相关文章

用代码构建未来,让技术改变世界 🚀 | 专注前端技术,分享开发经验