前言

在当今互联网时代,用户对页面加载速度的要求越来越高。据统计,页面加载时间每增加1秒,用户流失率会增加11%。作为前端开发者,掌握性能优化技能已经成为必备技能。本文将结合实际项目经验,详细介绍前端性能优化的各种方法。

一、性能指标与测量

1.1 关键性能指标

FCP (First Contentful Paint):首次内容绘制时间

  • 衡量页面开始加载到首个文本或图像显示的时间
  • 理想值:< 1.8秒

LCP (Largest Contentful Paint):最大内容绘制时间

  • 衡量页面主要内容完全加载的时间
  • 理想值:< 2.5秒

FID (First Input Delay):首次输入延迟

  • 衡量用户首次与页面交互到浏览器响应的时间
  • 理想值:< 100毫秒

CLS (Cumulative Layout Shift):累计布局偏移

  • 衡量页面元素意外移动的程度
  • 理想值:< 0.1

1.2 性能测量工具

// 使用 Web Vitals API 监控性能
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals'

const sendToAnalytics = (metric) => {
  console.log(`${metric.name}: ${metric.value}ms`)
  // 发送到分析服务
}

getCLS(sendToAnalytics)
getFID(sendToAnalytics)
getFCP(sendToAnalytics)
getLCP(sendToAnalytics)
getTTFB(sendToAnalytics)

二、资源加载优化

2.1 图片优化

响应式图片

<!-- 根据屏幕尺寸提供不同图片 -->
<picture>
  <source media="(max-width: 768px)" srcset="small.webp" type="image/webp">
  <source media="(max-width: 768px)" srcset="small.jpg" type="image/jpeg">
  <source srcset="large.webp" type="image/webp">
  <img src="large.jpg" alt="响应式图片" loading="lazy">
</picture>

图片懒加载

// 原生懒加载
<img src="image.jpg" loading="lazy" alt="懒加载图片">

// 自定义懒加载
const images = document.querySelectorAll('img[data-src]')
const imageObserver = new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target
      img.src = img.dataset.src
      img.classList.remove('lazy')
      observer.unobserve(img)
    }
  })
})

images.forEach(img => imageObserver.observe(img))

图片压缩与格式优化

// 自动图片压缩
const compressImage = (file, quality = 0.8, maxWidth = 1920) => {
  return new Promise((resolve) => {
    const canvas = document.createElement('canvas')
    const ctx = canvas.getContext('2d')
    const img = new Image()
    
    img.onload = () => {
      let { width, height } = img
      if (width > maxWidth) {
        height = (height * maxWidth) / width
        width = maxWidth
      }
      
      canvas.width = width
      canvas.height = height
      ctx.drawImage(img, 0, 0, width, height)
      canvas.toBlob(resolve, 'image/jpeg', quality)
    }
    
    img.src = URL.createObjectURL(file)
  })
}

2.2 代码分割与懒加载

路由级别代码分割

// Vue Router 懒加载
const routes = [
  {
    path: '/dashboard',
    component: () => import('@/views/Dashboard.vue')
  },
  {
    path: '/user',
    component: () => import('@/views/User.vue')
  }
]

// React Router 懒加载
import { lazy, Suspense } from 'react'
const Dashboard = lazy(() => import('./Dashboard'))

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Dashboard />
    </Suspense>
  )
}

组件级别懒加载

// Vue 3 异步组件
import { defineAsyncComponent } from 'vue'

const AsyncComponent = defineAsyncComponent({
  loader: () => import('./HeavyComponent.vue'),
  loadingComponent: LoadingComponent,
  errorComponent: ErrorComponent,
  delay: 200,
  timeout: 3000
})

2.3 资源预加载

关键资源预加载

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

<!-- 预连接 -->
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>

<!-- 资源预加载 -->
<link rel="preload" href="critical.css" as="style">
<link rel="preload" href="hero-image.jpg" as="image">

<!-- 页面预取 -->
<link rel="prefetch" href="/next-page.html">

三、渲染性能优化

3.1 虚拟滚动

<template>
  <div class="virtual-scroll" @scroll="handleScroll" ref="container">
    <div class="scroll-content" :style="{ height: totalHeight + 'px' }">
      <div 
        class="visible-items" 
        :style="{ transform: `translateY(${offsetY}px)` }"
      >
        <div 
          v-for="item in visibleItems" 
          :key="item.id"
          class="item"
          :style="{ height: itemHeight + 'px' }"
        >
          {{ item.content }}
        </div>
      </div>
    </div>
  </div>
</template>

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

const props = defineProps({
  items: Array,
  itemHeight: { type: Number, default: 50 },
  containerHeight: { type: Number, default: 400 }
})

const container = ref(null)
const scrollTop = ref(0)

const visibleCount = computed(() => 
  Math.ceil(props.containerHeight / props.itemHeight) + 2
)

const startIndex = computed(() => 
  Math.floor(scrollTop.value / props.itemHeight)
)

const endIndex = computed(() => 
  Math.min(startIndex.value + visibleCount.value, props.items.length)
)

const visibleItems = computed(() => 
  props.items.slice(startIndex.value, endIndex.value)
)

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

const offsetY = computed(() => 
  startIndex.value * props.itemHeight
)

const handleScroll = (e) => {
  scrollTop.value = e.target.scrollTop
}
</script>

3.2 防抖与节流

// 防抖:延迟执行,适用于搜索框输入
const debounce = (func, delay) => {
  let timeoutId
  return function (...args) {
    clearTimeout(timeoutId)
    timeoutId = setTimeout(() => func.apply(this, args), delay)
  }
}

// 节流:限制执行频率,适用于滚动事件
const throttle = (func, limit) => {
  let inThrottle
  return function (...args) {
    if (!inThrottle) {
      func.apply(this, args)
      inThrottle = true
      setTimeout(() => inThrottle = false, limit)
    }
  }
}

// 使用示例
const searchInput = document.getElementById('search')
const handleSearch = debounce((e) => {
  console.log('搜索:', e.target.value)
}, 300)

const handleScroll = throttle(() => {
  console.log('滚动位置:', window.scrollY)
}, 100)

searchInput.addEventListener('input', handleSearch)
window.addEventListener('scroll', handleScroll)

四、缓存策略

4.1 HTTP 缓存

强缓存配置

// Express 服务器缓存配置
app.use('/static', express.static('public', {
  maxAge: '1y', // 静态资源缓存1年
  etag: false
}))

// Nginx 配置
location ~* \.(js|css|png|jpg|jpeg|gif|svg|woff2)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

4.2 浏览器缓存

// localStorage 缓存工具
class CacheManager {
  constructor(maxSize = 50) {
    this.maxSize = maxSize
    this.cache = new Map()
  }
  
  set(key, value, ttl = 3600000) { // 默认1小时
    if (this.cache.size >= this.maxSize) {
      const firstKey = this.cache.keys().next().value
      this.cache.delete(firstKey)
    }
    
    const item = {
      value,
      timestamp: Date.now(),
      ttl
    }
    
    this.cache.set(key, item)
    localStorage.setItem(key, JSON.stringify(item))
  }
  
  get(key) {
    const item = this.cache.get(key) || 
                 JSON.parse(localStorage.getItem(key) || 'null')
    
    if (!item) return null
    
    if (Date.now() - item.timestamp > item.ttl) {
      this.delete(key)
      return null
    }
    
    return item.value
  }
  
  delete(key) {
    this.cache.delete(key)
    localStorage.removeItem(key)
  }
}

// 使用示例
const cache = new CacheManager()
cache.set('userInfo', { name: '张三', id: 123 }, 1800000) // 30分钟
const userInfo = cache.get('userInfo')

4.3 Service Worker 缓存

// service-worker.js
const CACHE_NAME = 'app-cache-v1'
const urlsToCache = [
  '/',
  '/static/css/main.css',
  '/static/js/main.js',
  '/images/logo.png'
]

// 安装 Service Worker
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then((cache) => cache.addAll(urlsToCache))
  )
})

// 拦截网络请求
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request)
      .then((response) => {
        // 缓存命中,返回缓存
        if (response) {
          return response
        }
        
        // 缓存未命中,发起网络请求
        return fetch(event.request)
          .then((response) => {
            if (!response || response.status !== 200) {
              return response
            }
            
            // 克隆响应并存入缓存
            const responseToCache = response.clone()
            caches.open(CACHE_NAME)
              .then((cache) => {
                cache.put(event.request, responseToCache)
              })
            
            return response
          })
      })
  )
})

五、JavaScript 性能优化

5.1 减少 DOM 操作

// ❌ 错误做法:频繁 DOM 操作
for (let i = 0; i < 1000; i++) {
  const div = document.createElement('div')
  div.textContent = `Item ${i}`
  document.body.appendChild(div)
}

// ✅ 正确做法:批量 DOM 操作
const fragment = document.createDocumentFragment()
for (let i = 0; i < 1000; i++) {
  const div = document.createElement('div')
  div.textContent = `Item ${i}`
  fragment.appendChild(div)
}
document.body.appendChild(fragment)

5.2 事件委托

// ❌ 错误做法:给每个元素绑定事件
document.querySelectorAll('.button').forEach(button => {
  button.addEventListener('click', handleClick)
})

// ✅ 正确做法:事件委托
document.addEventListener('click', (e) => {
  if (e.target.classList.contains('button')) {
    handleClick(e)
  }
})

5.3 Web Workers

// main.js
const worker = new Worker('worker.js')

// 发送大量数据给 Worker 处理
worker.postMessage({
  command: 'processData',
  data: largeDataArray
})

worker.onmessage = (e) => {
  const { result } = e.data
  console.log('处理结果:', result)
}

// worker.js
self.onmessage = (e) => {
  const { command, data } = e.data
  
  if (command === 'processData') {
    // 执行耗时计算
    const result = data.map(item => {
      // 复杂计算逻辑
      return item * 2
    })
    
    self.postMessage({ result })
  }
}

六、网络优化

6.1 HTTP/2 与压缩

// 启用 gzip 压缩
const compression = require('compression')
app.use(compression())

// Brotli 压缩配置
const express = require('express')
const compression = require('compression')

app.use(compression({
  level: 6,
  threshold: 1024,
  filter: (req, res) => {
    if (req.headers['x-no-compression']) {
      return false
    }
    return compression.filter(req, res)
  }
}))

6.2 请求优化

// 请求合并
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 results = await Promise.all(requests)
      
      batch.forEach((item, index) => {
        item.resolve(results[index])
      })
    } catch (error) {
      batch.forEach(item => item.reject(error))
    }
  }
}

// 使用示例
const batcher = new RequestBatcher()
batcher.add(fetch('/api/user/1'))
batcher.add(fetch('/api/user/2'))

七、实际项目应用案例

7.1 医疗系统性能优化实战

在我参与的临床科研一体化项目中,面对10万+条患者记录的查询挑战,采用了以下优化策略:

虚拟滚动 + 分页优化

<template>
  <div class="patient-list">
    <virtual-scroll 
      :items="patientData"
      :item-height="60"
      :container-height="500"
      @load-more="loadMoreData"
    >
      <template #default="{ item }">
        <patient-card :patient="item" />
      </template>
    </virtual-scroll>
  </div>
</template>

<script setup>
// 分页加载患者数据
const loadMoreData = async () => {
  const response = await api.getPatients({
    page: currentPage.value,
    size: 20
  })
  patientData.value.push(...response.data)
  currentPage.value++
}
</script>

智能缓存策略

// 患者数据缓存管理
class PatientDataCache {
  constructor() {
    this.cache = new Map()
    this.maxAge = 30 * 60 * 1000 // 30分钟
  }
  
  async getPatient(id) {
    const cached = this.cache.get(id)
    if (cached && Date.now() - cached.timestamp < this.maxAge) {
      return cached.data
    }
    
    const data = await api.getPatientDetail(id)
    this.cache.set(id, {
      data,
      timestamp: Date.now()
    })
    
    return data
  }
}

7.2 优化效果

通过综合性能优化,项目取得了显著效果:

  • 首屏加载时间从 3.2s 优化到 1.9s(提升 40%)
  • 大表格渲染时间从 2.8s 优化到 1.2s(提升 57%)
  • 页面切换时间从 1.5s 优化到 0.8s(提升 47%)

八、性能监控与分析

8.1 性能监控系统

// 性能监控类
class PerformanceMonitor {
  constructor() {
    this.metrics = {}
    this.init()
  }
  
  init() {
    // 监控页面加载性能
    window.addEventListener('load', () => {
      setTimeout(() => this.collectLoadMetrics(), 0)
    })
    
    // 监控用户交互性能
    this.observeUserInteraction()
    
    // 监控资源加载
    this.observeResourceLoading()
  }
  
  collectLoadMetrics() {
    const navigation = performance.getEntriesByType('navigation')[0]
    const paint = performance.getEntriesByType('paint')
    
    this.metrics = {
      // 页面加载时间
      loadTime: navigation.loadEventEnd - navigation.loadEventStart,
      // DOM 解析时间
      domParseTime: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart,
      // 首次绘制时间
      firstPaint: paint.find(p => p.name === 'first-paint')?.startTime,
      // 首次内容绘制时间
      firstContentfulPaint: paint.find(p => p.name === 'first-contentful-paint')?.startTime
    }
    
    this.sendMetrics()
  }
  
  observeUserInteraction() {
    ['click', 'scroll', 'keypress'].forEach(event => {
      document.addEventListener(event, (e) => {
        const startTime = performance.now()
        
        requestAnimationFrame(() => {
          const endTime = performance.now()
          const interactionTime = endTime - startTime
          
          if (interactionTime > 100) {
            console.warn(`${event} 交互延迟过高: ${interactionTime}ms`)
          }
        })
      })
    })
  }
  
  observeResourceLoading() {
    const observer = new PerformanceObserver((list) => {
      list.getEntries().forEach((entry) => {
        if (entry.duration > 1000) {
          console.warn(`资源加载过慢: ${entry.name}, 耗时: ${entry.duration}ms`)
        }
      })
    })
    
    observer.observe({ entryTypes: ['resource'] })
  }
  
  sendMetrics() {
    // 发送性能数据到分析服务
    fetch('/api/analytics/performance', {
      method: 'POST',
      body: JSON.stringify(this.metrics)
    })
  }
}

// 启动性能监控
new PerformanceMonitor()

九、阶段总结

9.1 开发阶段

  1. 代码分割:合理拆分代码包,避免单个包过大
  2. 懒加载:非关键资源采用懒加载策略
  3. 压缩优化:启用代码压缩和图片压缩
  4. 缓存策略:合理设置缓存策略

9.2 构建阶段

// vite.config.js 优化配置
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'vue-vendor': ['vue', 'vue-router', 'pinia'],
          'ui-vendor': ['element-plus'],
          'utils': ['axios', 'dayjs']
        }
      }
    },
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true
      }
    }
  },
  plugins: [
    // 代码压缩
    compression(),
    // 包分析
    bundleAnalyzer()
  ]
})

9.3 运行时优化

  1. 避免内存泄漏:及时清理事件监听器和定时器
  2. 减少重绘重排:批量 DOM 操作,使用 CSS3 动画
  3. 优化算法:选择合适的数据结构和算法
  4. 性能监控:建立完善的性能监控体系

十、总结

前端性能优化是一个系统性工程,需要从多个维度进行考虑:

  1. 加载性能:通过资源优化、缓存策略提升加载速度
  2. 渲染性能:通过虚拟滚动、防抖节流优化渲染效率
  3. 交互性能:通过合理的事件处理和异步操作提升响应速度
  4. 网络性能:通过请求优化和压缩减少网络开销

性能优化没有银弹,需要根据具体项目和用户场景选择合适的优化策略。同时,性能优化是一个持续的过程,需要通过监控和分析不断发现问题和改进方案。

希望这份指南能够帮助大家在实际项目中实现更好的性能表现,提升用户体验。记住,每一毫秒的优化都可能带来更好的用户留存率!


如果这篇文章对你有帮助,欢迎点赞收藏,也欢迎在评论区分享你的性能优化经验!

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐