性能优化整体思路

优化的本质是减少关键路径上的工作量。按用户体感分三个阶段:加载(白屏到可交互)、渲染(页面流畅度)、运行时(交互响应)。

一、加载阶段:让首屏更快

1. 资源体积瘦身

1
2
3
4
5
6
# 生产构建开启压缩
# Vite:vite-plugin-compression 生成 .gz/.br 文件
# Nginx 配合:
gzip on;
gzip_types text/css application/javascript application/json image/svg+xml;
gzip_min_length 1k;

图片是体积大户:能 WebP/AVIF 就用,能 CDN 就上 CDN,能懒加载就懒加载。

2. 缓存策略

1
2
3
4
5
6
7
8
9
10
# 带 hash 的静态资源:长缓存
location /assets/ {
expires 30d;
add_header Cache-Control "public, immutable";
}

# 入口 html:不缓存,保证发布后用户拿到新版本
location / {
add_header Cache-Control "no-cache";
}

3. 懒加载:图片和路由

1
2
3
4
5
<!-- 图片懒加载:进入视口才加载 -->
<img loading="lazy" src="photo.jpg" alt="" />

<!-- iframe 懒加载 -->
<iframe loading="lazy" src="https://example.com"></iframe>
1
2
3
// 路由懒加载(Vue)
const Admin = () => import('./views/Admin.vue')
// 配合 webpackChunkName 控制分包命名

4. 预加载关键资源

1
2
3
4
5
<!-- 首屏关键字体/图片提前加载 -->
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin />

<!-- 下一屏页面提前预热(用户即将点击时) -->
<link rel="prefetch" href="/about" />

5. 减少请求数

  • 小图标合并成 SVG sprite 或直接用 iconfont/icon 组件;
  • 小的装饰图片 base64 内联(阈值 8KB 左右);
  • API 合并:首屏多个接口能合并就合并,减少往返。

二、渲染阶段:让页面不卡

1. 减少重排(Layout)

重排代价极高,常见的”罪魁祸首”:

1
2
3
4
5
6
7
// 反模式:循环里反复读写布局属性,强制同步重排
for (let i = 0; i < items.length; i++) {
el.style.left = items[i].x + 'px' // 每行都触发重排
}

// 好做法:用 transform 做动画(GPU 合成,不触发重排)
el.style.transform = `translateX(${items[i].x}px)`

经验法则:动画用 transform + opacity,别用 top/left/width。

2. 虚拟列表:长列表必备

渲染 1 万个 DOM 节点页面必卡。虚拟列表只渲染可视区域的节点:

1
2
3
4
// 简单思路(社区库 vue-virtual-scroller / react-window 同理)
// 1. 计算可视区起始/结束 index
// 2. 只渲染 [start, end] 之间的项
// 3. 外层容器 padding 撑出总高度,保持滚动条正确

业务里出现”一次性渲染几百上千条数据”的列表,直接上虚拟列表。

3. 减少不必要的渲染

1
2
3
4
5
6
7
8
9
// React:memo 防止无关 props 变化触发渲染
const Item = React.memo(function Item({ data }) {
return <div>{data.name}</div>
})

// Vue:computed 缓存计算结果,避免模板里反复计算
const filteredList = computed(() =>
list.value.filter((i) => i.visible)
)

4. 防抖节流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 防抖:停止触发后才执行(搜索输入)
function debounce(fn, delay = 300) {
let timer
return (...args) => {
clearTimeout(timer)
timer = setTimeout(() => fn(...args), delay)
}
}

// 节流:固定频率执行(滚动、resize)
function throttle(fn, interval = 100) {
let last = 0
return (...args) => {
const now = Date.now()
if (now - last >= interval) {
last = now
fn(...args)
}
}
}

搜索用防抖,滚动用节流,别搞反。

三、运行时优化

1. 事件委托

1
2
3
4
5
6
7
8
9
10
// 反模式:1000 个 li 每个绑一个监听
document.querySelectorAll('.item').forEach((el) =>
el.addEventListener('click', handler)
)

// 好做法:一个监听挂到父元素
list.addEventListener('click', (e) => {
const item = e.target.closest('.item')
if (item) handler(item)
})

2. 大数据计算放 Worker

1
2
3
4
5
6
// 主线程不卡
const worker = new Worker('compute.js')
worker.postMessage(bigData)
worker.onmessage = (e) => {
// 拿到计算结果
}

图片压缩、加密、复杂排序这类 CPU 密集任务丢给 Worker。

3. 内存注意

  • 组件卸载时清掉定时器和事件监听(React useEffect 清理函数、Vue onUnmounted);
  • 大对象用后置空引用;
  • 用 WeakMap/WeakSet 缓存数据,避免强引用阻止 GC。

四、测量:先量化再优化

1
2
# 性能预算检查
npx lighthouse https://example.com --preset=desktop

Chrome DevTools 三个关键面板:

  • Performance:录制交互,看长任务(Long Task)和主线程占用;
  • Network:看阻塞时间和资源体积;
  • Lighthouse:Core Web Vitals 分数(LCP、INP、CLS)。

优化顺序建议:LCP(首屏最大内容)→ 图片体积 → 缓存 → 懒加载 → 长列表虚拟化 → 动画 transform 化。先测后改,改完复测。

常见坑

  1. 只优化不测量:凭感觉改,可能白忙;先记录优化前后指标;
  2. 过度懒加载:首屏关键图片/内容被懒加载反而拖慢 LCP;
  3. 缓存配错:html 也长缓存,导致发布后用户一直看旧版本;
  4. 虚拟列表滚动抖动:行高不定时高度估算不准确,建议固定行高或预测量。

总结

性能优化是系统工程:加载阶段抓体积和缓存,渲染阶段抓重排和渲染数量,运行时抓事件和内存。记住”先测量、再优化、后复测”的铁律,对照 Core Web Vitals 逐个击破。