性能优化
性能指标体系
RAIL 模型(Google)
| 维度 |
含义 |
目标 |
获取方式 |
| Response |
响应用户输入 |
点击/触摸后 < 100ms 给出视觉反馈 |
Performance Observer |
| Animation |
动画帧 |
每帧 < 16ms(60fps) |
requestAnimationFrame 回调间隔 |
| Idle |
空闲时段 |
将延迟任务分散到空闲时间片(< 50ms/块) |
requestIdleCallback |
| Load |
加载完成 |
首次加载 < 5s(3G 慢速),后续加载 < 2s |
Navigation Timing API |
Core Web Vitals(核心网页指标)
| 指标 |
全称 |
含义 |
良好 |
一般 |
差 |
获取方式 |
| LCP |
Largest Contentful Paint |
最大内容绘制(可见区最大元素渲染时间) |
< 2.5s |
2.5s~4.0s |
> 4.0s |
PerformanceObserver largest-contentful-paint |
| INP |
Interaction to Next Paint |
交互到下一次绘制的延迟 — 取代 FID |
< 200ms |
200ms~500ms |
> 500ms |
PerformanceObserver first-input(实验) |
| CLS |
Cumulative Layout Shift |
累计布局偏移 — 衡量视觉稳定性 |
< 0.1 |
0.1~0.25 |
> 0.25 |
PerformanceObserver layout-shift |
// 收集 Core Web Vitals
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.entryType === 'largest-contentful-paint') {
console.log('LCP:', entry.startTime);
}
if (entry.entryType === 'layout-shift') {
console.log('CLS:', entry.value);
}
}
});
observer.observe({ type: 'largest-contentful-paint', buffered: true });
observer.observe({ type: 'layout-shift', buffered: true });
其他关键指标
| 指标 |
含义 |
良好阈值 |
| FCP |
First Contentful Paint — 首次内容绘制 |
< 1.8s |
| TTFB |
Time to First Byte — 首字节时间(服务端响应 + 网络延迟) |
< 800ms |
| TBT |
Total Blocking Time — FCP 到 TTI 之间所有长任务阻塞时间总和 |
< 200ms |
| SI |
Speed Index — 页面内容可见填充速度(Lighthouse) |
< 3.4s |
| TTI |
Time to Interactive — 可交互时间 |
< 3.8s |
- Lighthouse: 通过 Chrome DevTools / CLI
lighthouse <url> 生成报告,评分基于加权组合
- Performance API:
performance.timing(已弃用)→ performance.getEntriesByType('navigation')(Navigation Timing Level 2)
- web-vitals 库:
npm install web-vitals — 封装好的 onLCP() onINP() onCLS() onFCP() onTTFB()
网络优化
HTTP 缓存策略
┌─────────────┐
请求 ──────────►│ 强缓存 │◄── Cache-Control / Expires
└──────┬──────┘
│ 未命中
▼
┌─────────────┐
│ 协商缓存 │◄── ETag / Last-Modified
└──────┬──────┘
│ 未修改
▼
304 Not Modified
| 场景 |
Cache-Control |
说明 |
| 静态资源(JS/CSS/图片) |
public, max-age=31536000, immutable |
配合 content hash 文件名实现永久缓存 |
| HTML |
no-cache 或 no-store |
不缓存或每次校验 |
| API 响应 |
private, max-age=60 |
仅允许浏览器缓存,不允许 CDN |
# 静态资源推荐
Cache-Control: public, max-age=31536000, immutable
# HTML
Cache-Control: no-cache
# API 带认证
Cache-Control: private, max-age=60
压缩
| 算法 |
压缩比(文本) |
压缩速度 |
解压速度 |
浏览器支持 |
| Gzip |
~70% |
快 |
快 |
所有浏览器 |
| Brotli |
~80% |
慢(需要更多 CPU) |
快(解压接近 gzip) |
现代浏览器(HTTPS 下) |
- Brotli 比 Gzip 小 15~25%,优先在 HTTPS 中使用
- Nginx 配置
brotli on; + brotli_static on; 使用预压缩 .br 文件
CDN 策略
- 预热: 大版本发布前主动请求所有静态资源 URL,拉回到边缘节点缓存
- 刷新: 资源回退或紧急更新时清除 CDN 缓存,配合
?v= 参数或 hash
- 多级缓存: CDN Edge → CDN L1/L2 → Origin,减少回源
资源预加载
<!-- DNS 预解析 -->
<link rel="dns-prefetch" href="//api.example.com">
<!-- 预连接(包含 DNS + TCP + TLS) -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<!-- 预加载关键资源(高优先级,当前页需要) -->
<link rel="preload" href="critical-font.woff2" as="font" crossorigin>
<!-- 预获取(低优先级,下一页可能用) -->
<link rel="prefetch" href="/next-page.js">
<!-- 预渲染(更高成本,渲染整个页面) -->
<link rel="prerender" href="/next-page.html">
| 类型 |
优先级 |
适用场景 |
是否当前页 |
| preload |
高(as 指定类型决定) |
关键字体、首屏图片、关键 CSS |
当前页 |
| preconnect |
中 |
第三方域名提前建立连接 |
当前页 |
| prefetch |
低 |
下面几个页面的资源 |
未来页 |
| dns-prefetch |
最低 |
只需 DNS 查询的域名 |
当前/未来 |
HTTP 升级
| 版本 |
核心改进 |
状态 |
| HTTP/1.1 |
队头阻塞(HOL)、连接数限制(浏览器通常 6 个/域名) |
逐步淘汰 |
| HTTP/2 |
多路复用(单一 TCP 连接传输多个流)、HPACK 头部压缩、Server Push |
主流,Server Push 已废弃(Chrome 106 移除,因为推送资源命中率低,不如 preload) |
| HTTP/3 |
QUIC 基于 UDP 实现、0-RTT 握手、无队头阻塞(丢包不影响其他流)、连接迁移 |
逐步普及 |
Server Push 废弃原因: 推送的资源即使浏览器已有缓存也会被推过来,浪费带宽;且 Service Worker 无法拦截;不如 preload + 103 Early Hints。
雪碧图 / Base64 / SVG Sprite 对比
| 方案 |
适用场景 |
优点 |
缺点 |
| CSS Sprite(雪碧图) |
大量小图标(< 5KB 级别) |
减少 HTTP 请求 |
维护困难、缩放模糊 |
| Base64 |
极小图片(< 2KB),内联在 CSS 中 |
无额外请求 |
体积增大 33%、CSS 文件膨胀、不缓存 |
| SVG Sprite |
矢量图标 |
清晰、可缩放、可 CSS 控制颜色 |
复杂图形无法使用 |
资源优化
图片优化
格式选择
| 格式 |
压缩率 |
透明 |
动画 |
浏览器支持 |
| JPEG |
基准 |
否 |
否 |
全部 |
| PNG |
无损 |
是 |
否 |
全部 |
| WebP |
比 JPEG 小 25~35% |
是 |
是 |
95%+ |
| AVIF |
比 JPEG 小 50%+ |
是 |
是 |
~90%(Safari 16+) |
策略: 优先 AVIF,WebP 兜底,JPEG 作为最后兼容
<picture>
<source srcset="image.avif" type="image/avif">
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="fallback" loading="lazy" decoding="async">
</picture>
响应式图片
<img
srcset="
image-320w.jpg 320w,
image-640w.jpg 640w,
image-1280w.jpg 1280w
"
sizes="(max-width: 640px) 100vw, (max-width: 1200px) 50vw, 33vw"
src="image-1280w.jpg"
alt="responsive"
>
srcset 的 w 描述符告诉浏览器图片真实宽度
sizes 告知浏览器在不同视口下图片的显示宽度
- 浏览器根据 DPR + 视口宽度 + sizes 自动选择最合适的资源
Lazy Loading
<!-- 浏览器原生懒加载 -->
<img src="..." loading="lazy" decoding="async">
<!-- Intersection Observer 高级控制 -->
<img class="lazy" data-src="real.jpg" alt="">
<script>
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.src = entry.target.dataset.src;
observer.unobserve(entry.target);
}
});
});
document.querySelectorAll('img.lazy').forEach(img => observer.observe(img));
</script>
loading="lazy": 浏览器处理,简单但无法精确控制
IntersectionObserver: 更灵活,可设置 rootMargin 提前加载
字体优化
| 策略 |
实现 |
效果 |
font-display: swap |
字体加载期间先显示 fallback 字体 |
避免 FOIT(Flash of Invisible Text),但可能 FOUT |
font-display: optional |
字体如果在 100ms 内没加载完成,使用 fallback |
弱网下不阻塞渲染 |
| 字体子集化 |
unicode-range + 工具(glyphhanger)只加载用到的字符 |
中文字体可从数 MB 降到几十 KB |
| WOFF2 |
优于 WOFF 的压缩格式 |
压缩比最大 |
| preload 关键字体 |
<link rel="preload" as="font"> |
尽早开始字体下载 |
@font-face {
font-family: 'MyFont';
src: url('/fonts/myfont.woff2') format('woff2');
font-display: swap;
unicode-range: U+4E00-9FFF; /* 仅加载 CJK 字符 */
}
JS Bundle 优化
Tree Shaking
- 依赖 ES Module 静态结构(
import/export 而非 require)
sideEffects: false 在 package.json 中标记无副作用
- Webpack/Rollup 在 production 模式自动 tree-shake
// package.json
{
"sideEffects": [
"*.css",
"*.global.js"
]
}
Code Splitting
┌──────────────────────┐
│ 入口 main.js │
├──────────────────────┤
│ Vendor (react, vue…) │ ── splitChunks
├──────────────────────┤
│ 路由级 chunk │ ── dynamic import
│ - /home -> Home.js │
│ - /about -> About.js │
│ - /detail -> Detail.js│
└──────────────────────┘
// React 动态导入
const Home = React.lazy(() => import('./pages/Home'));
// Vue 动态导入
const Home = () => import('./pages/Home.vue');
// Webpack splitChunks 配置
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
构建配置优化(Webpack 5)
| 特性 |
说明 |
splitChunks |
提取公共代码到独立 chunk |
externals |
将 React/Vue 等大库标记为外部依赖(通过 CDN 加载) |
| DLL(已废弃) |
Webpack 5 已移除 DLLPlugin,改用 splitChunks + persistent caching |
编译加速
| 工具 |
语言 |
替代场景 |
快多少 |
| esbuild |
Go |
Babel / Terser / TS 编译 |
10~100x |
| SWC |
Rust |
Babel / Terser |
20x+ |
| Turbopack |
Rust |
Webpack(Next.js 集成) |
实验性 |
// esbuild-loader(Webpack + esbuild)
{
test: /\.ts$/,
loader: 'esbuild-loader',
options: { loader: 'tsx', target: 'es2020' }
}
依赖优化
| 优化 |
说明 |
webpack-bundle-analyzer |
分析 bundle 组成,定位体积大的依赖 |
| lodash-es |
按需导入,支持 tree-shaking(import { debounce } from 'lodash-es') |
| moment → dayjs |
dayjs 仅 2KB,API 兼容 moment |
| date-fns |
函数式日期工具,按需引入,tree-shakable |
| 移除 polyfill |
core-js 按需引入,避免全部打包 |
渲染性能
关键渲染路径(Critical Rendering Path)
HTML ──► DOM
│
CSS ────► CSSOM ──► Render Tree ──► Layout ──► Paint ──► Composite
│ │ │ │
解析 CSS 合并 DOM+CSSOM 计算布局 绘制像素 合并图层
优化内联关键 CSS:
<!-- 将首屏关键 CSS 内联在 <head> 中 -->
<style>
/* 首屏样式,直接内联 */
.header { position: fixed; top: 0; ... }
.hero { height: 100vh; ... }
</style>
<!-- 非关键 CSS 异步加载 -->
<link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
重排(Reflow)与重绘(Repaint)
| 操作 |
触发 |
成本 |
| 重排 (Reflow/Layout) |
改变元素几何属性(width/height/padding/margin/position) |
高 — 涉及 Layout → Paint → Composite |
| 重绘 (Repaint) |
改变视觉但不影响布局(color/background/visibility) |
中 — 涉及 Paint → Composite |
| 合成 (Composite) |
仅改变 transform/opacity |
低 — 只涉及 Composite |
最佳实践:
// ❌ 频繁触发重排
el.style.width = '100px';
el.style.height = '200px';
el.style.margin = '10px';
// ✅ 使用 class 批量修改
el.classList.add('active');
// ✅ 使用 transform/opacity 动画(仅触发合成)
el.style.transform = 'translateX(100px)';
el.style.opacity = '0.5';
// ✅ 批量读取时先读再写(避免强制重排)
const h = el.clientHeight; // 读
el.style.height = h + 10 + 'px'; // 写
requestAnimationFrame:
- 每一帧浏览器渲染前执行,与显示器刷新率同步(通常 60Hz)
- 用于修改样式、更新动画,避免掉帧
- 不可见页面自动暂停,节省资源
可视区 ┌────────────┐
│ 上缓冲区 │ ← 提前渲染,保证快速滚动进入的可视性
├────────────┤
│ 可视区渲染 │ ← 用户看到的部分
├────────────┤
│ 下缓冲区 │ ← 提前渲染
└────────────┘
↑ ↑
scrollTop scrollTop+clientHeight
- 仅渲染可视区 + 上下缓冲区(通常各 5~10 个项目)
- 监听
scroll 事件计算当前可视范围
- DOM 节点池回收复用,不随数据增长
- 知名库:
react-window / vue-virtual-scroller / @tanstack/react-virtual
// 核心逻辑示意
function VirtualList({ items, itemHeight, containerHeight }) {
const [scrollTop, setScrollTop] = useState(0);
const startIdx = Math.floor(scrollTop / itemHeight);
const endIdx = Math.min(
startIdx + Math.ceil(containerHeight / itemHeight) + bufferSize,
items.length
);
const visibleItems = items.slice(startIdx, endIdx);
return (
<div style={{ height: containerHeight, overflow: 'auto' }} onScroll={...}>
<div style={{ height: items.length * itemHeight, position: 'relative' }}>
{visibleItems.map((item, i) => (
<div key={item.id} style={{
position: 'absolute',
top: (startIdx + i) * itemHeight,
height: itemHeight
}}>
{item.content}
</div>
))}
</div>
</div>
);
}
防抖(Debounce)与节流(Throttle)
| 控制方式 |
行为 |
推荐场景 |
推荐值 |
| 防抖 |
停止触发后 N ms 执行一次 |
输入搜索、窗口 resize |
输入 300ms |
| 节流 |
固定时间间隔内最多执行一次 |
滚动监听、鼠标移动 |
滚动 100ms |
// Lodash 防抖:输入建议
input.addEventListener('input', debounce(handleInput, 300));
// Lodash 节流:滚动监听
window.addEventListener('scroll', throttle(handleScroll, 100));
Web Worker
- 运行在独立线程,不阻塞主 UI 线程
- 通过
postMessage 通信
- 适用场景: 图像处理、大数据排序、编解码、复杂计算
- 限制: 无 DOM 访问、无 window/document,不能跨域加载脚本
// main.js
const worker = new Worker('compute.js');
worker.postMessage({ data: largeArray });
worker.onmessage = (e) => {
console.log('结果:', e.data);
};
// compute.js
self.onmessage = (e) => {
const result = complexCalculation(e.data);
self.postMessage(result);
};
OffscreenCanvas(离屏渲染)
- 在 Web Worker 中使用 Canvas 渲染
- 使用
OffscreenCanvas 替代主线程 Canvas
- 适用于复杂图形渲染(图表、游戏)
// 主线程
const canvas = document.getElementById('canvas');
const offscreen = canvas.transferControlToOffscreen();
const worker = new Worker('renderer.js');
worker.postMessage({ canvas: offscreen }, [offscreen]);
// renderer.js
self.onmessage = (e) => {
const canvas = e.data.canvas;
const ctx = canvas.getContext('2d');
// 在 Worker 中渲染
};
框架优化
React 优化
React.memo / useMemo / useCallback
| 手段 |
作用 |
何时使用 |
React.memo |
组件级缓存:props 未变不重渲染 |
纯展示组件、props 频繁引用相等但值不变 |
useMemo |
缓存计算结果 |
复杂计算、避免对象/数组引用变化导致子组件重渲染 |
useCallback |
缓存函数引用 |
传给子组件的回调 props |
// ✅ 使用 React.memo 避免不必要的重新渲染
const ExpensiveList = React.memo(({ items }) => {
return items.map(item => <div key={item.id}>{item.name}</div>);
});
// ✅ useMemo 缓存复杂计算
const sortedItems = useMemo(() => {
return items.sort((a, b) => a.price - b.price);
}, [items]);
// ✅ useCallback 保持函数引用稳定
const handleClick = useCallback((id) => {
setSelected(prev => prev === id ? null : id);
}, []);
// ❌ 避免内联创建对象和函数
<ExpensiveList items={items} onClick={(id) => handleClick(id)} />
// ↑ 每次渲染创建新函数,破坏 memo
// ✅ 改为 useCallback + 在子组件内部解构
Vue 优化
| 手段 |
说明 |
场景 |
计算属性 computed |
惰性求值,依赖不变不重算 |
派生状态、复杂表达式 |
shallowRef |
仅追踪 .value 的引用变化,不深层响应 |
大数据列表、不可变数据 |
v-memo |
缓存模板片段(Vue 3.2+) |
固定数据的列表渲染 |
<script setup>
// ✅ computed 优于模板表达式
const fullName = computed(() => `${firstName.value} ${lastName.value}`);
// ✅ shallowRef 处理大数据集
const list = shallowRef(bigArray); // 整体替换时触发更新,不深层响应
// ❌ watch 用于有副作用的场景,不要用于派生数据
// ❌ 模板中的复杂表达式应提取到 computed
</script>
computed vs watch: computed 是惰性求值的派生声明,watch 是侦测变化后执行副作用。能用 computed 就不用 watch。
Suspense + Lazy
<template>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<Loading />
</template>
</Suspense>
</template>
React 版:
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<Suspense fallback={<Spinner />}>
<OtherComponent />
</Suspense>
);
}
构建优化
| 手段 |
配置 |
效果 |
| 代码分割 |
splitChunks |
分离 vendor / 公共模块 |
| externals |
externals: { react: 'React' } |
通过 CDN 加载,减少 bundle |
| esbuild/SWC 编译 |
esbuild-loader / swc-loader |
替代 babel-loader,编译速度 10~20x |
| 产物分析 |
webpack-bundle-analyzer |
发现冗余依赖 |
| tree shaking |
sideEffects: false |
删除 dead code |
性能监控
// Navigation Timing
const nav = performance.getEntriesByType('navigation')[0];
const ttfb = nav.responseStart - nav.requestStart;
const fcp = nav.domContentLoadedEventEnd - nav.fetchStart;
// PerformanceObserver 监听各种性能事件
const observer = new PerformanceObserver((list) => {
// long task 检测
for (const entry of list.getEntries()) {
if (entry.entryType === 'longtask') {
console.warn(`Long Task detected: ${entry.duration}ms`, entry);
}
}
});
observer.observe({ type: 'longtask', buffered: true });
监控工具
| 工具 |
用途 |
类型 |
| Lighthouse CI |
CI 中集成性能测试,设置阈值 |
Synthetic |
| WebPageTest |
多地域/多设备/基于真实浏览器的性能分析 |
Synthetic |
| web-vitals |
收集真实用户 Core Web Vitals |
RUM |
| Sentry Performance |
错误 + 性能追踪 |
RUM |
火焰图(Flame Chart)解读
时间 ────────────────────────────────────────────►
┌──────────────────────────────────────────────────┐
│ Main ── 线程活动 │
│ ┌──────┐ ┌───────┐ ┌────┐ ┌─────────┐ │
│ │Task A│ │ Task B│ │Idle│ │ Task C │ │
│ │ ├───┐│ │ │ │ │ │ ├───┐ │ │
│ │ │sub││ │ │ │ │ │ │sub│ │ │
│ └──┴───┘│ └───────┘ └────┘ │ └───┘ │ │
└─────────┴─────────────────┴─────────┴──────────┘
宽度 = 耗时 高度 = 调用栈深度
- 宽度表示函数执行时间,越宽越耗时
- 高度表示调用栈深度,右上角的红色三角标记为长任务(> 50ms)
- Summary 面板: 各类型活动占比(Scripting/Rendering/Painting/System/Idle)
长任务定位
- 在 Performance 面板录制
- 查看 Main 线程中的红色角标任务(> 50ms)
- 点击查看调用栈,定位具体函数
- Bottom-Up 标签页按时间倒序排列最耗时的函数
网络与模拟
- Network Throttling: 模拟 Slow 3G / Fast 3G / Offline
- CPU Throttling: 模拟低端设备(4x / 6x slowdown)
- Disable Cache: 模拟首次加载
查看重绘与 Layer
| 功能 |
入口 |
用途 |
| Paint Flashing |
Rendering → Paint flashing |
高亮显示重绘区域 |
| Layer Borders |
Rendering → Layer borders |
查看合成层(可优化复合) |
| FPS Meter |
Rendering → FPS Meter |
实时帧率 |
| Scrolling Performance Issues |
Rendering |
滚动卡顿排查 |