前端开发

前端性能优化实战:从首屏渲染到交互流畅

2025-07-20 wangjun 16 min read
<>

性能优化的两个维度

用户感知的性能分两段:加载性能(页面多久能看)与运行性能(操作是否跟手)。前者由关键渲染路径决定,后者由渲染/布局成本决定。先量化再优化,避免拍脑袋。

关键渲染路径(CRP)

HTML → CSSOM → Render Tree → Layout → Paint → Composite
  ↓        ↓
阻塞渲染   阻塞渲染

浏览器拿到 HTML 后要构建 DOM 和 CSSOM,两者齐备才首次渲染。优化要点:

  • CSS 放 head、JS 放 body 底部:script 会阻塞解析,能用 defer 就不要同步加载
  • 首屏关键 CSS 内联:把首屏样式内联,其余异步加载
  • 图片loading="lazy" 延迟加载、srcset 按屏宽选图、WebP 格式
<script defer src="app.js"></script>
<link rel="preload" href="hero.webp" as="image">
<img src="hero.webp" alt="" loading="lazy">

资源加载策略

# 构建产物体积控制
1. 代码分割:路由级按需加载(React.lazy)
2. Tree Shaking:只打包用到的导出
3. 压缩:gzip/brotli(通常能省 60%+ 传输体积)

# HTTP 缓存
- 静态资源: Cache-Control: public, max-age=31536000, immutable
- HTML:      Cache-Control: no-cache(保证更新可达)
- 文件名带 hash:内容变化则 URL 变化,缓存自动失效

运行时性能:让操作不卡顿

浏览器一帧只有 16.6ms,脚本、布局、绘制超时就掉帧:

  • 避免布局抖动(Layout Thrashing):不要在循环里交替读 offsetWidth 再改样式,先批量读、再批量写
  • 动画只动 transform/opacity:它们走合成层,不触发布局和绘制
  • 事件节流防抖scrollresizeinput 高频事件必须限频
  • 长任务拆分:超过 50ms 的任务用 requestIdleCallbacksetTimeout 分片
// 防抖:输入停止后才执行
function debounce(fn, wait = 300) {
  let t;
  return (...args) => {
    clearTimeout(t);
    t = setTimeout(() => fn(...args), wait);
  };
}


function throttle(fn, interval = 100) {
  let last = 0;
  return (...args) => {
    const now = Date.now();
    if (now - last >= interval) { last = now; fn(...args); }
  };
}

长列表:虚拟滚动

渲染 1 万条 DOM 必然卡死。虚拟滚动只渲染视口内的条目,用 padding 撑起总高度:

// 思路:只渲染可见区 ~30 条
const visible = items.slice(start, end);
return (
  <div style={{ height: totalHeight, overflow: "auto" }}>
    <div style={{ transform: translateY(start * rowHeight) }}>
      {visible.map(renderItem)}
    </div>
  </div>
);

量化验证:别凭感觉

# Lighthouse 五个核心指标
LCP  首次内容渲染最大元素   < 2.5s
INP  交互到下一次绘制       < 200ms
CLS  累积布局偏移           < 0.1
FCP  首次内容绘制           < 1.8s
TTFB 服务器响应时间         < 800ms

每次改动后跑一次 Lighthouse 与性能录制,对比指标变化,让优化可度量、可回归。

性能优化不是一次性手术,而是持续的健康管理 -- 指标先行,量入为出。