2024年10月28日9 分钟

前端性能优化全指南:从加载到运行时的系统性优化

系统性梳理前端性能优化的方方面面,涵盖网络层、资源加载、渲染性能、运行时优化和监控体系

性能优化的度量指标

优化前先建立度量标准,否则就是盲目优化。

Core Web Vitals(核心网页指标)

LCP(Largest Contentful Paint)最大内容绘制
  衡量:页面主要内容何时可见
  目标:< 2.5 秒
  影响因素:图片加载、字体加载、服务端响应时间

FID(First Input Delay)首次输入延迟
  衡量:用户首次交互到浏览器响应的延迟
  目标:< 100ms
  影响因素:主线程阻塞、长任务、JS 执行时间

CLS(Cumulative Layout Shift)累积布局偏移
  衡量:页面内容是否意外跳动
  目标:< 0.1
  影响因素:图片无尺寸、动态插入内容、字体切换

完整指标体系

页面加载时序:
  │
  ├── TTFB (Time to First Byte)     ← 服务器响应速度
  ├── FP (First Paint)               ← 首次绘制(可能是白屏→有色)
  ├── FCP (First Contentful Paint)   ← 首次有意义内容
  ├── LCP (Largest Contentful Paint) ← 最大内容元素
  ├── FID (First Input Delay)        ← 首次可交互延迟
  └── TTI (Time to Interactive)      ← 完全可交互

一、网络层优化

1. 资源压缩

// next.config.js — 启用压缩
module.exports = {
  compress: true,  // 默认启用 gzip

  // webpack 配置 Brotli 压缩(比 gzip 小 15-25%)
  webpack: (config) => {
    if (!config.plugins) config.plugins = []
    // 生产环境使用 Brotli
    return config
  },
}
文件大小对比:
  bundle.js    → 500KB(原始)
  bundle.js.gz → 150KB(gzip,压缩 70%)
  bundle.js.br → 120KB(brotli,压缩 76%)

2. HTTP/2 和 HTTP/3

HTTP/1.1 的问题:
  浏览器对同一域名最多 6 个并发连接
  请求必须排队(队头阻塞)

HTTP/2 优化:
  ✅ 多路复用:一个连接上并行传输多个请求
  ✅ 头部压缩:减少重复 header
  ✅ 服务器推送:主动推送资源

HTTP/3 (QUIC):
  ✅ 基于 UDP,无队头阻塞
  ✅ 0-RTT 建连
  ✅ 更好的弱网表现

3. 资源预加载

<!-- DNS 预解析:提前解析第三方域名 -->
<link rel="dns-prefetch" href="https://api.example.com" />

<!-- 预连接:DNS + TCP + TLS 提前完成 -->
<link rel="preconnect" href="https://fonts.googleapis.com" />

<!-- 预加载:关键资源优先下载 -->
<link rel="preload" href="/fonts/inter.woff2" as="font" crossorigin />

<!-- 预获取:下一页可能用到的资源 -->
<link rel="prefetch" href="/next-page-bundle.js" />

二、资源加载优化

1. 代码分割(Code Splitting)

// 路由级分割
import { lazy, Suspense } from 'react'

const Dashboard = lazy(() => import('./pages/Dashboard'))
const Settings = lazy(() => import('./pages/Settings'))

// 组件级分割
const HeavyChart = lazy(() => import('./components/HeavyChart'))

function Page() {
  return (
    <Suspense fallback={<Skeleton />}>
      <HeavyChart />
    </Suspense>
  )
}
分割前:bundle.js (800KB) — 首页加载全部代码
分割后:
  main.js     (200KB) — 首页核心代码
  dashboard.js (300KB) — 进入 Dashboard 才加载
  settings.js  (150KB) — 进入 Settings 才加载
  chart.js     (150KB) — 图表可见时才加载

2. 图片优化

// Next.js Image 组件自动优化
import Image from 'next/image'

<Image
  src="/hero.jpg"
  width={1200}
  height={600}
  alt="Hero"
  priority          // 首屏图片:预加载
  placeholder="blur" // 模糊占位
  blurDataURL="..."  // base64 缩略图
  sizes="(max-width: 768px) 100vw, 50vw"  // 响应式尺寸
/>

// 自动实现:
// ✅ WebP/AVIF 格式转换(比 JPEG 小 25-50%)
// ✅ 响应式图片(不同屏幕加载不同尺寸)
// ✅ 懒加载(非首屏图片滚动到可视区才加载)
// ✅ 缓存优化

3. 字体优化

// ❌ 外部字体阻塞渲染
<link href="https://fonts.googleapis.com/..." rel="stylesheet">

// ✅ next/font 自动优化
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'], display: 'swap' })

// 效果:
// 构建时下载字体 → 自托管 → 零外部请求
// font-display: swap → 先显示系统字体,字体加载后切换

4. Tree Shaking

// ❌ 导入整个库
import _ from 'lodash'          // 打包整个 lodash: 70KB
_.debounce(fn, 300)

// ✅ 按需导入
import debounce from 'lodash/debounce'  // 只打包 debounce: 2KB

// ✅ 使用支持 Tree Shaking 的库
import { debounce } from 'lodash-es'    // ES Module,webpack 自动摇树

三、渲染性能优化

1. 避免不必要的重渲染

// React.memo:浅比较 props
const ExpensiveList = React.memo(({ items }) => {
  return items.map(item => <Item key={item.id} {...item} />)
})

// useMemo:缓存计算结果
const sorted = useMemo(() =>
  items.sort((a, b) => b.score - a.score),
[items])

// useCallback:缓存函数引用
const handleClick = useCallback((id) => {
  setSelected(id)
}, [])

2. 虚拟列表(大数据量渲染)

import { FixedSizeList } from 'react-window'

// ❌ 直接渲染 10000 条数据 → 创建 10000 个 DOM 节点 → 卡顿
{data.map(item => <Row key={item.id} {...item} />)}

// ✅ 虚拟列表:只渲染可视区域内的 20-30 条
<FixedSizeList
  height={600}
  itemCount={data.length}    // 10000 条数据
  itemSize={50}              // 每行高度
>
  {({ index, style }) => (
    <div style={style}>
      {data[index].name}
    </div>
  )}
</FixedSizeList>
// 只渲染 ~15 个 DOM 节点,滚动时复用

3. 避免布局抖动(Layout Thrashing)

// ❌ 读写交替 → 强制同步布局
elements.forEach(el => {
  const height = el.offsetHeight     // 读(触发布局计算)
  el.style.height = height + 10 + 'px' // 写(使布局失效)
  // 下一次读又触发重新计算...循环 N 次
})

// ✅ 批量读,批量写
const heights = elements.map(el => el.offsetHeight)  // 统一读
elements.forEach((el, i) => {
  el.style.height = heights[i] + 10 + 'px'           // 统一写
})

4. 防抖和节流

// 防抖(Debounce):停止触发后才执行
// 适用:搜索输入、窗口 resize
function debounce(fn: Function, delay: number) {
  let timer: NodeJS.Timeout
  return (...args: any[]) => {
    clearTimeout(timer)
    timer = setTimeout(() => fn(...args), delay)
  }
}

// 节流(Throttle):固定间隔执行一次
// 适用:滚动事件、鼠标移动
function throttle(fn: Function, interval: number) {
  let last = 0
  return (...args: any[]) => {
    const now = Date.now()
    if (now - last >= interval) {
      last = now
      fn(...args)
    }
  }
}

四、构建优化

Bundle 分析

# 分析打包体积
npx next build
npx @next/bundle-analyzer

# 或使用 webpack-bundle-analyzer
ANALYZE=true npm run build
分析结果示例:
  ├── framework (react + react-dom): 130KB
  ├── commons (shared chunks): 50KB
  ├── pages/index: 25KB
  ├── pages/blog: 15KB
  └── node_modules:
      ├── framer-motion: 32KB  ← 是否可以用 CSS 替代?
      ├── date-fns: 12KB      ← 只用了 format,是否可以手写?
      └── react-markdown: 20KB

依赖优化

// next.config.js
module.exports = {
  experimental: {
    optimizePackageImports: [
      'lucide-react',      // 只打包用到的图标
      'date-fns',          // 只打包用到的函数
      'framer-motion',     // 只打包用到的 API
    ],
  },
}

五、监控体系

Web Vitals 上报

// lib/analytics.ts
import { onCLS, onFID, onLCP, onFCP, onTTFB } from 'web-vitals'

function reportMetric(metric) {
  // 上报到你的分析平台
  fetch('/api/analytics', {
    method: 'POST',
    body: JSON.stringify({
      name: metric.name,        // 'CLS' | 'FID' | 'LCP'
      value: metric.value,      // 具体数值
      rating: metric.rating,    // 'good' | 'needs-improvement' | 'poor'
      navigationType: metric.navigationType,
    }),
  })
}

onCLS(reportMetric)
onFID(reportMetric)
onLCP(reportMetric)
onFCP(reportMetric)
onTTFB(reportMetric)

优化检查清单

网络层:
  □ 启用 Brotli/Gzip 压缩
  □ 使用 CDN 分发静态资源
  □ 启用 HTTP/2
  □ 配置合理的缓存策略
  □ 关键资源 preload/preconnect

资源加载:
  □ 路由级代码分割
  □ 图片格式优化(WebP/AVIF)
  □ 图片懒加载
  □ 字体自托管 + display:swap
  □ Tree Shaking 验证

渲染性能:
  □ React.memo 避免无效渲染
  □ useMemo/useCallback 合理使用
  □ 虚拟列表处理大数据
  □ 避免布局抖动

构建优化:
  □ Bundle 体积分析
  □ 移除未使用依赖
  □ 按需导入第三方库

监控:
  □ Core Web Vitals 采集上报
  □ Lighthouse CI 定期检测
  □ 性能预算告警

总结

性能优化是一个系统工程,需要从网络、资源、渲染、构建四个层面综合考虑。核心原则是:先度量、再优化、持续监控。避免过早优化,用数据驱动决策。