Web 性能优化实战:从 70 分到 95 分的优化之旅

Web 性能优化实战:从 70 分到 95 分的优化之旅

背景

最近接手了一个性能问题严重的项目,Lighthouse 评分只有 70 分,首屏加载时间超过 5 秒。经过一个月的优化,最终将评分提升到 95 分,首屏加载时间降到 1.2 秒。本文将分享整个优化过程。

性能分析

初始状态

使用 Chrome DevTools 和 Lighthouse 进行初步分析,发现以下问题:

  1. 包体积过大:主包达到 2.5MB
  2. 未优化的图片:大量未压缩的高清图片
  3. 阻塞渲染的资源:多个同步加载的第三方脚本
  4. 内存泄漏:长时间使用后内存占用持续增长

性能指标

初始性能指标:

  • FCP (First Contentful Paint): 3.2s
  • LCP (Largest Contentful Paint): 5.1s
  • TTI (Time to Interactive): 7.8s
  • CLS (Cumulative Layout Shift): 0.25

优化策略

1. 代码分割和懒加载

首先对路由进行代码分割:

// 使用 React.lazy 进行路由级别的代码分割
const Dashboard = lazy(() => import('./pages/Dashboard'))
const Analytics = lazy(() => import('./pages/Analytics'))
const Settings = lazy(() => import('./pages/Settings'))

function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/analytics" element={<Analytics />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  )
}

对大型第三方库进行按需加载:

// 只在需要时加载 Chart.js
async function loadChartLibrary() {
  const { Chart } = await import('chart.js/auto')
  return Chart
}

function ChartComponent({ data }) {
  useEffect(() => {
    loadChartLibrary().then(Chart => {
      // 使用 Chart.js
    })
  }, [])
}

2. 图片优化

实施多维度的图片优化策略:

// 使用 next/image 自动优化图片
import Image from 'next/image'

function OptimizedImage({ src, alt }) {
  return (
    <Image
      src={src}
      alt={alt}
      width={800}
      height={600}
      loading="lazy"
      placeholder="blur"
      blurDataURL={generateBlurDataURL(src)}
    />
  )
}

// 根据设备提供不同尺寸的图片
function ResponsiveImage({ src, alt }) {
  return (
    <picture>
      <source
        media="(max-width: 640px)"
        srcSet={`${src}?w=640&q=75`}
      />
      <source
        media="(max-width: 1024px)"
        srcSet={`${src}?w=1024&q=75`}
      />
      <img
        src={`${src}?w=1920&q=75`}
        alt={alt}
        loading="lazy"
      />
    </picture>
  )
}

3. 资源加载优化

优化关键资源的加载顺序:

<!-- 预连接到关键域名 -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="dns-prefetch" href="https://api.example.com">

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

<!-- 延迟加载非关键脚本 -->
<script src="/js/analytics.js" defer></script>
<script src="/js/chat-widget.js" async></script>

4. 运行时性能优化

优化 React 组件的渲染性能:

// 使用 memo 避免不必要的重渲染
const ExpensiveComponent = memo(({ data }) => {
  return <ComplexVisualization data={data} />
}, (prevProps, nextProps) => {
  // 自定义比较逻辑
  return prevProps.data.id === nextProps.data.id
})

// 使用 useMemo 缓存计算结果
function DataProcessor({ rawData }) {
  const processedData = useMemo(() => {
    return expensiveProcessing(rawData)
  }, [rawData])
  
  return <DataDisplay data={processedData} />
}

// 使用 useCallback 避免函数重创建
function SearchBar({ onSearch }) {
  const [query, setQuery] = useState('')
  
  const debouncedSearch = useCallback(
    debounce((value) => onSearch(value), 300),
    [onSearch]
  )
  
  return (
    <input
      value={query}
      onChange={(e) => {
        setQuery(e.target.value)
        debouncedSearch(e.target.value)
      }}
    />
  )
}

5. 缓存策略

实施多层缓存策略:

// Service Worker 缓存策略
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((response) => {
      // 缓存优先,网络回退
      return response || fetch(event.request).then((response) => {
        // 缓存新资源
        if (response.status === 200) {
          const responseClone = response.clone()
          caches.open('v1').then((cache) => {
            cache.put(event.request, responseClone)
          })
        }
        return response
      })
    })
  )
})

// HTTP 缓存头配置
app.use((req, res, next) => {
  // 静态资源长期缓存
  if (req.url.match(/\.(js|css|jpg|png|gif|ico|woff2)$/)) {
    res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
  }
  // API 响应短期缓存
  else if (req.url.startsWith('/api/')) {
    res.setHeader('Cache-Control', 'private, max-age=300')
  }
  next()
})

6. 内存泄漏修复

定位并修复内存泄漏:

// 错误示例:忘记清理事件监听器
function LeakyComponent() {
  useEffect(() => {
    const handler = () => console.log('scroll')
    window.addEventListener('scroll', handler)
    // 忘记清理!
  }, [])
}

// 正确示例:清理副作用
function FixedComponent() {
  useEffect(() => {
    const handler = () => console.log('scroll')
    window.addEventListener('scroll', handler)
    
    return () => {
      window.removeEventListener('scroll', handler)
    }
  }, [])
}

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

function getCachedData(obj) {
  if (cache.has(obj)) {
    return cache.get(obj)
  }
  
  const data = processObject(obj)
  cache.set(obj, data)
  return data
}

优化结果

经过一个月的优化,各项指标显著提升:

性能指标对比

指标优化前优化后提升
FCP3.2s0.8s75%
LCP5.1s1.2s76%
TTI7.8s2.1s73%
CLS0.250.0292%
包体积2.5MB780KB69%

用户体验提升

  • 跳出率降低 45%:用户更愿意等待页面加载
  • 转化率提升 23%:更快的交互响应提高了用户满意度
  • 移动端体验改善:在 3G 网络下也能流畅使用

持续监控

建立性能监控体系:

// 使用 Performance Observer API 监控性能
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    // 上报性能数据
    analytics.track('performance', {
      name: entry.name,
      duration: entry.duration,
      startTime: entry.startTime
    })
  }
})

observer.observe({ entryTypes: ['navigation', 'resource', 'paint'] })

// 监控长任务
const taskObserver = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.duration > 50) {
      console.warn('Long task detected:', entry)
    }
  }
})

taskObserver.observe({ entryTypes: ['longtask'] })

经验总结

  1. 先测量后优化:使用工具准确定位问题,避免盲目优化
  2. 关注用户体验:性能优化的最终目标是提升用户体验
  3. 渐进式优化:一次只优化一个方面,便于评估效果
  4. 自动化监控:建立监控体系,防止性能退化
  5. 团队协作:制定性能预算,让整个团队关注性能

工具推荐

  • 分析工具:Lighthouse、WebPageTest、Chrome DevTools
  • 监控服务:Sentry、DataDog、New Relic
  • 优化工具:Webpack Bundle Analyzer、source-map-explorer
  • 图片优化:Squoosh、ImageOptim、TinyPNG

性能优化是一个持续的过程,需要在开发的每个阶段都保持关注。希望这些经验能帮助你优化自己的项目!