前端性能调优实战指南 --- 22 条优化策略
1. React.memo 避免无效重渲染
分类:React / 渲染优化
优化前
- 父组件 state 变化,200 个子组件全部重渲染
- Profiler 单次渲染耗时 120ms
- 搜索输入有明显卡顿
tsx
// 每次父组件 re-render,RowItem 都会重新渲染
function RowItem({ data }: { data: RowData }) {
return <div>{data.name} - {data.value}</div>;
}
优化后
- React.memo 包裹子组件,props 不变则跳过渲染
- 渲染耗时降至 8ms
- 搜索输入丝滑流畅
tsx
const RowItem = React.memo(function RowItem({ data }: { data: RowData }) {
return <div>{data.name} - {data.value}</div>;
});
const filteredList = useMemo(
() => list.filter(item => item.name.includes(keyword)),
[list, keyword]
);
对比图
xychart-beta
title "渲染耗时对比"
x-axis ["优化前", "优化后"]
y-axis "耗时 (ms)" 0 --> 130
bar [120, 8]
| 指标 | 优化前 | 优化后 | 变化 |
|---|---|---|---|
| 渲染耗时 | 120ms | 8ms | 减少了 93% |
| 重渲染组件数 | 200 个 | 5 个 | 减少了 96% |
2. 虚拟滚动长列表
分类:DOM 优化
优化前
- 5000+ 行数据全量 DOM 渲染
- DOM 节点数 15,000+
- 首屏渲染 3.2s ,滚动仅 12fps
tsx
function DataTable({ rows }: { rows: DataRow[] }) {
return (
<div className="table-body">
{rows.map(row => (
<div key={row.id} className="table-row">
<span>{row.name}</span>
<span>{row.status}</span>
</div>
))}
</div>
);
}
优化后
- react-window 只渲染可视区域内的行
- DOM 节点数 ~60
- 首屏 200ms ,滚动稳定 60fps
tsx
import { FixedSizeList } from 'react-window';
function DataTable({ rows }: { rows: DataRow[] }) {
return (
<FixedSizeList height={600} itemCount={rows.length} itemSize={48} width="100%">
{({ index, style }) => (
<div style={style} className="table-row">
<span>{rows[index].name}</span>
<span>{rows[index].status}</span>
</div>
)}
</FixedSizeList>
);
}
对比图
xychart-beta
title "DOM 节点数对比"
x-axis ["优化前", "优化后"]
y-axis "节点数" 0 --> 16000
bar [15000, 60]
xychart-beta
title "滚动帧率对比 (fps)"
x-axis ["优化前", "优化后"]
y-axis "fps" 0 --> 65
bar [12, 60]
| 指标 | 优化前 | 优化后 | 变化 |
|---|---|---|---|
| DOM 节点 | 15,000+ | ~60 | 减少了 99.6% |
| 首屏渲染 | 3.2s | 200ms | 减少了 94% |
| 滚动帧率 | 12fps | 60fps | 提升了 5 倍 |
3. 路由级代码分割
分类:构建优化
优化前
- 所有页面打包为单个 main.js
- 首屏 JS 体积 860KB (gzip)
- 3G 网络首屏白屏 4.5s
tsx
import HomePage from './pages/home';
import DashboardPage from './pages/dashboard';
import SettingsPage from './pages/settings';
优化后
- React.lazy 按路由动态导入,按需加载
- 首屏 JS 体积 210KB
- 首屏加载 1.8s
tsx
const HomePage = lazy(() => import('./pages/home'));
const DashboardPage = lazy(() => import('./pages/dashboard'));
const SettingsPage = lazy(() => import('./pages/settings'));
<Suspense fallback={<PageSkeleton />}>
<HomePage />
</Suspense>
对比图
xychart-beta
title "首屏 JS 体积对比 (KB, gzip)"
x-axis ["优化前", "优化后"]
y-axis "体积 (KB)" 0 --> 900
bar [860, 210]
gantt
title 资源加载瀑布流对比
dateFormat X
axisFormat %s
section 优化前
main.js 860KB (阻塞 4.5s) :crit, 0, 45
section 优化后
vendor.js 120KB :active, 0, 12
home.js 90KB :active, 0, 9
dashboard.js (按需) :done, 18, 18
| 指标 | 优化前 | 优化后 | 变化 |
|---|---|---|---|
| 首屏 JS | 860KB | 210KB | 减少了 76% |
| 首屏加载时间 | 4.5s | 1.8s | 减少了 60% |
4. 接口防抖 + 缓存
分类:网络请求 / 缓存策略
优化前
- 每次键入字符立即请求 API
- 输入 11 字符触发 11 次请求
- 服务端峰值 800 QPS/min
tsx
const handleChange = async (e) => {
const value = e.target.value;
setKeyword(value);
const res = await fetchSearchResults(value); // 每次输入都请求
setResults(res.data);
};
优化后
- 300ms 防抖 + React Query 5 分钟缓存
- 同样输入仅 1-2 次请求
- 服务端降至 80 QPS/min
tsx
const debouncedKeyword = useDebouncedValue(keyword, 300);
const { data: results = [] } = useQuery({
queryKey: ['search', debouncedKeyword],
queryFn: () => fetchSearchResults(debouncedKeyword),
enabled: debouncedKeyword.length > 0,
staleTime: 5 * 60 * 1000,
});
对比图
xychart-beta
title "输入 performance 触发的 API 请求数"
x-axis ["优化前", "优化后"]
y-axis "请求数" 0 --> 12
bar [11, 2]
| 指标 | 优化前 | 优化后 | 变化 |
|---|---|---|---|
| 请求数 | 11 次 | 1-2 次 | 减少了 90% |
| 服务端 QPS | 800/min | 80/min | 减少了 90% |
| 缓存命中延迟 | 200ms | 0ms | 减少了 100% |
5. 图片 WebP + 懒加载
分类:资源优化
优化前
- 20 张 PNG Banner,均值 350KB/张
- 总体积 7MB,首屏一次性加载
- LCP 5.8s
html
<img src="/images/banner1.png" />
<!-- 20 张全部在首屏加载 -->
优化后
- WebP 格式 +
loading="lazy"原生懒加载 - 均值 85KB/张,首屏仅加载 3 张
- LCP 2.1s
tsx
<picture>
<source srcSet={banner.webpUrl} type="image/webp" />
<img src={banner.pngUrl} loading="lazy" decoding="async" />
</picture>
对比图
pie title 优化前图片体积分布
"首屏加载 (3张)" : 1050
"其余加载 (17张)" : 5950
pie title 优化后图片体积分布
"首屏加载 (3张)" : 255
"懒加载 (17张)" : 1445
"体积节省" : 5300
| 指标 | 优化前 | 优化后 | 变化 |
|---|---|---|---|
| 图片体积 | 7MB | 1.7MB | 减少了 76% |
| LCP | 5.8s | 2.1s | 减少了 64% |
6. useMemo 缓存昂贵计算
分类:React / 计算优化
优化前
- 10000 条数据每次渲染都重新过滤、分组、聚合
- 计算耗时 65ms
- 切换 Tab 有明显延迟
tsx
function Dashboard({ rawData, activeTab }: Props) {
const chartData = processRawData(rawData); // 每次渲染都执行 45ms
const summary = calculateSummary(rawData); // 每次渲染都执行 20ms
}
优化后
- useMemo 缓存,仅在 rawData 变化时重新计算
- 缓存命中耗时 0.1ms
- 切换 Tab 即时响应
tsx
const chartData = useMemo(() => processRawData(rawData), [rawData]);
const summary = useMemo(() => calculateSummary(rawData), [rawData]);
对比图
xychart-beta
title "切换 Tab 时计算耗时 (ms)"
x-axis ["优化前", "优化后"]
y-axis "耗时 (ms)" 0 --> 70
bar [65, 0.1]
| 指标 | 优化前 | 优化后 | 变化 |
|---|---|---|---|
| 计算耗时 | 65ms | 0.1ms | 减少了 99% |
7. Tree Shaking 按需导入
分类:构建优化 / 依赖瘦身
优化前
- lodash 全量导入 + moment.js
- 仅用 3 个函数,却引入 530KB
- 占总 bundle 35%
tsx
import _ from 'lodash';
import moment from 'moment';
优化后
- lodash 路径导入 + dayjs 替代 moment
- 两库合计 28KB
- 占总 bundle 2.5%
tsx
import groupBy from 'lodash/groupBy';
import sortBy from 'lodash/sortBy';
import dayjs from 'dayjs';
对比图
pie title 优化前 Bundle 组成
"lodash + moment" : 530
"其他依赖" : 985
pie title 优化后 Bundle 组成
"lodash/* + dayjs" : 28
"其他依赖" : 985
| 指标 | 优化前 | 优化后 | 变化 |
|---|---|---|---|
| 依赖体积 | 530KB | 28KB | 减少了 95% |
| 占 bundle 比例 | 35% | 2.5% | 减少了 93% |
8. 状态管理下沉
分类:React / 状态管理
优化前
- 表单每个字段的临时值都存 Redux 全局 store
- 输入触发 47 个 组件重渲染(含导航栏、侧边栏)
- 输入延迟 80ms,打字卡顿
tsx
const value = useSelector((state) => state.form[fieldName]);
const dispatch = useDispatch();
<input value={value} onChange={e => dispatch(updateFormField(fieldName, e.target.value))} />
优化后
- 表单状态下沉到局部 useState,提交时才同步到全局
- 仅 3 个 组件重渲染
- 输入延迟 4ms
tsx
const [formData, setFormData] = useState<FormData>(initialData);
const handleSubmit = () => dispatch(submitForm(formData));
对比图
xychart-beta
title "单次输入触发的组件渲染数"
x-axis ["优化前 (Redux)", "优化后 (useState)"]
y-axis "组件数" 0 --> 50
bar [47, 3]
| 指标 | 优化前 | 优化后 | 变化 |
|---|---|---|---|
| 渲染组件数 | 47 | 3 | 减少了 94% |
| 输入延迟 | 80ms | 4ms | 减少了 95% |
9. CSS transform 替代位置属性动画
分类:CSS / 动画流畅度
优化前
- 抽屉展开/收起使用
left属性做动画 - 每帧触发 Layout → Paint → Composite(三阶段全走)
- 帧率 24fps ,CPU 65%
less
.drawer {
left: -400px;
transition: left 0.3s ease;
&.open { left: 0; } // 触发 Layout 重排
}
优化后
- 改用
transform: translateX(),仅触发 Composite - 帧率 60fps ,CPU 12%
less
.drawer {
transform: translateX(-100%);
transition: transform 0.3s ease;
will-change: transform;
&.open { transform: translateX(0); } // 仅触发 Composite
}
对比图
graph LR
subgraph 优化前 - left 属性
A1[Layout 重排] --> A2[Paint 重绘] --> A3[Composite 合成]
end
subgraph 优化后 - transform
B1[Composite 合成]
end
style A1 fill:#e74c3c,color:#fff
style A2 fill:#e67e22,color:#fff
style A3 fill:#f1c40f,color:#333
style B1 fill:#27ae60,color:#fff
xychart-beta
title "动画帧率对比 (fps)"
x-axis ["优化前 (left)", "优化后 (transform)"]
y-axis "fps" 0 --> 65
bar [24, 60]
| 指标 | 优化前 | 优化后 | 变化 |
|---|---|---|---|
| 帧率 | 24fps | 60fps | 提升了 150% |
| CPU 占用 | 65% | 12% | 减少了 82% |
10. 事件委托替代逐项绑定
分类:JavaScript / 内存优化
优化前
- 500 行 × 3 按钮 = 1500 个事件监听器
- 初始化耗时 90ms ,内存多占 12MB
tsx
{items.map(item => (
<div key={item.id}>
<button onClick={() => handleView(item.id)}>查看</button>
<button onClick={() => handleEdit(item.id)}>编辑</button>
<button onClick={() => handleDelete(item.id)}>删除</button>
</div>
))}
优化后
- 父容器事件委托,通过
data-*识别目标 - 仅 1 个监听器,初始化 0.5ms
tsx
<div onClick={handleAction}>
{items.map(item => (
<div key={item.id}>
<button data-action="view" data-id={item.id}>查看</button>
<button data-action="edit" data-id={item.id}>编辑</button>
<button data-action="delete" data-id={item.id}>删除</button>
</div>
))}
</div>
对比图
xychart-beta
title "事件监听器数量"
x-axis ["优化前 (逐项绑定)", "优化后 (事件委托)"]
y-axis "监听器数量" 0 --> 1600
bar [1500, 1]
| 指标 | 优化前 | 优化后 | 变化 |
|---|---|---|---|
| 监听器数 | 1,500 | 1 | 减少了 99.9% |
| 初始化耗时 | 90ms | 0.5ms | 减少了 99% |
| 内存占用 | 12MB | ~1MB | 减少了 92% |
11. Web Worker 卸载 CPU 密集型任务
分类:JavaScript / 用户体验
优化前
- 50000 行 Excel 在主线程解析、校验、转换
- 主线程阻塞 3.8s,页面完全冻结
tsx
const handleFileUpload = async (file: File) => {
const rawData = await readExcelFile(file); // 800ms
const validated = validateRows(rawData); // 1500ms
const transformed = transformData(validated); // 1500ms --- 期间页面冻结
};
优化后
- 计算逻辑移至 Web Worker,主线程始终可交互
- 主线程阻塞 0ms,有实时进度条
tsx
// worker.ts
self.onmessage = async (e) => {
const validated = validateRows(e.data.rawData);
self.postMessage({ type: 'progress', value: 50 });
const transformed = transformData(validated);
self.postMessage({ type: 'result', data: transformed });
};
对比图
gantt
title 主线程占用时间线
dateFormat X
axisFormat %s
section 优化前
主线程阻塞 3.8s (页面冻结) :crit, 0, 38
section 优化后
主线程空闲 (可正常交互) :active, 0, 38
Worker 处理数据 :done, 0, 32
| 指标 | 优化前 | 优化后 | 变化 |
|---|---|---|---|
| 主线程阻塞 | 3.8s | 0ms | 减少了 100% |
| 处理速度 | 3.8s | 3.2s | 提升了 15% |
12. HTTP 请求合并
分类:网络请求 / 首屏体验
优化前
- 看板初始化 8 个独立 API,受浏览器并发限制分 2 批
- 瀑布流总耗时 1200ms
- HTTP 头冗余 16KB
tsx
const [sales, orders, users, ...] = await Promise.all([
fetch('/api/dashboard/sales'),
fetch('/api/dashboard/orders'),
// ...共 8 个请求
]);
优化后
- 后端提供批量接口,1 次请求返回全部数据
- 总耗时 350ms ,头开销 2KB
tsx
const data = await fetch('/api/dashboard/batch', {
method: 'POST',
body: JSON.stringify({ modules: ['sales', 'orders', 'users', ...] }),
});
对比图
gantt
title 请求瀑布流对比
dateFormat X
axisFormat %s
section 优化前 (8个请求)
sales :crit, 0, 15
orders :crit, 0, 18
users :crit, 0, 12
inventory :crit, 0, 20
reviews :crit, 0, 16
traffic :crit, 20, 38
returns :crit, 20, 33
conversion:crit, 20, 36
section 优化后 (1个请求)
/batch :active, 0, 10
| 指标 | 优化前 | 优化后 | 变化 |
|---|---|---|---|
| 请求数 | 8 | 1 | 减少了 87.5% |
| 加载时间 | 1200ms | 350ms | 减少了 71% |
13. 大文件分片上传
分类:文件上传 / 网络优化
优化前
- 500MB 视频文件单次 POST 直传
- 上传成功率仅 62%
- 断网后需从头重传全部 500MB
- 超 100MB 触发 Nginx 413 错误
tsx
const formData = new FormData();
formData.append('file', file); // 500MB 一次性发送
await fetch('/api/upload', { method: 'POST', body: formData });
优化后
- 5MB 分片 + 3 路并发 + 断点续传
- 上传成功率 99.5%
- 断网恢复后只续传剩余分片
- 上传耗时从 180s 降至 72s
tsx
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
// 查询已上传分片 → 跳过 → 并发上传剩余 → 合并
对比图
xychart-beta
title "500MB 文件上传成功率 (%)"
x-axis ["优化前 (直传)", "优化后 (分片)"]
y-axis "成功率 (%)" 0 --> 105
bar [62, 99]
xychart-beta
title "500MB 文件上传耗时 (s)"
x-axis ["优化前", "优化后"]
y-axis "耗时 (s)" 0 --> 200
bar [180, 72]
graph LR
subgraph 分片上传断点续传示意
C1[1 ✅] --> C2[2 ✅] --> C3[3 ✅] --> C4[4 ✅] --> C5[5 ✅] --> C6[6 ⚠️断网]
C6 --> C7[7 🔄] --> C8[8 🔄] --> C9[9 🔄] --> C10[10 ⬜] --> C11[11 ⬜] --> C12[12 ⬜]
end
style C1 fill:#27ae60,color:#fff
style C2 fill:#27ae60,color:#fff
style C3 fill:#27ae60,color:#fff
style C4 fill:#27ae60,color:#fff
style C5 fill:#27ae60,color:#fff
style C6 fill:#e67e22,color:#fff
style C7 fill:#3498db,color:#fff
style C8 fill:#3498db,color:#fff
style C9 fill:#3498db,color:#fff
style C10 fill:#ddd,color:#999
style C11 fill:#ddd,color:#999
style C12 fill:#ddd,color:#999
| 指标 | 优化前 | 优化后 | 变化 |
|---|---|---|---|
| 上传成功率 | 62% | 99.5% | 提升了 60% |
| 上传耗时 | 180s | 72s | 减少了 60% |
| 断网重传量 | 100% | ~5% | 减少了 95% |
14. 客户端图片压缩
分类:文件上传 / Canvas API
优化前
- 10 张手机照片原图直传(4032×3024)
- 均值 5MB/张,总计 50MB
- 4G 网络上传耗时 45s
tsx
Array.from(files).forEach(file => {
formData.append('images', file); // 每张 4-6MB 原图
});
优化后
- Canvas 压缩 + 缩放到 1920px + quality 0.8
- 均值 300KB/张,总计 3MB
- 上传耗时 3s
tsx
function compressImage(file: File, maxWidth = 1920, quality = 0.8): Promise<Blob> {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
const ratio = Math.min(1, maxWidth / img.width);
canvas.width = img.width * ratio;
canvas.height = img.height * ratio;
canvas.getContext('2d')!.drawImage(img, 0, 0, canvas.width, canvas.height);
canvas.toBlob(blob => resolve(blob!), 'image/jpeg', quality);
};
img.src = URL.createObjectURL(file);
});
}
对比图
xychart-beta
title "10 张图片上传体积 (MB)"
x-axis ["优化前 (原图)", "优化后 (压缩)"]
y-axis "体积 (MB)" 0 --> 55
bar [50, 3]
| 指标 | 优化前 | 优化后 | 变化 |
|---|---|---|---|
| 上传体积 | 50MB | 3MB | 减少了 94% |
| 上传时间 | 45s | 3s | 减少了 93% |
| 存储成本 | 50MB/批 | 3MB/批 | 减少了 94% |
15. DNS 预解析 + 预连接
分类:网络优化 / 首屏体验
优化前
- 首次请求需完整建立连接
- DNS(80ms) + TCP(80ms) + TLS(120ms) = 280ms
- 5 个域名累计延迟 1.4s
html
<!-- 无任何预处理,连接在使用时才建立 -->
<link rel="stylesheet" href="https://cdn.example.com/styles/main.css" />
优化后
- dns-prefetch + preconnect 提前建立连接
- 首次请求连接延迟 ~0ms
html
<link rel="dns-prefetch" href="//cdn.example.com" />
<link rel="preconnect" href="https://cdn.example.com" crossorigin />
<link rel="preload" href="https://cdn.example.com/fonts/main.woff2" as="font" type="font/woff2" crossorigin />
对比图
gantt
title 单域名首次连接耗时分解
dateFormat X
axisFormat %s
section 优化前
DNS 解析 80ms :crit, 0, 8
TCP 握手 80ms :crit, 8, 16
TLS 协商 120ms :crit, 16, 28
section 优化后
预建立完成 0ms :active, 0, 1
| 指标 | 优化前 | 优化后 | 变化 |
|---|---|---|---|
| 单域名连接延迟 | 280ms | ~0ms | 减少了 100% |
| 5域名累计延迟 | 1.4s | ~0ms | 减少了 100% |
16. 前端缓存分层策略
分类:HTTP 缓存
优化前
- Cache-Control: no-cache,无缓存策略
- 每次访问重新下载 3.2MB
- 用户日均 8 次访问消耗 25.6MB/天
优化后
- 带 hash 静态资源强缓存 1 年 + HTML 协商缓存 + Service Worker
- 二次访问仅加载 ~5KB(HTML)
- CDN 带宽消耗 减少 85%
nginx
# 静态资源强缓存
location ~* \.(js|css|png|woff2)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
# HTML 协商缓存
location ~* \.html$ {
add_header Cache-Control "no-cache";
etag on;
}
对比图
xychart-beta
title "二次访问资源加载耗时 (ms)"
x-axis ["优化前 (无缓存)", "优化后 (缓存命中)"]
y-axis "耗时 (ms)" 0 --> 2000
bar [1800, 120]
graph TB
subgraph 缓存策略层次
SW[Service Worker
离线可用] --> Strong[强缓存 immutable
JS / CSS / 图片 / 字体] Strong --> Negotiate[协商缓存 ETag
HTML 入口文件] end style SW fill:#3498db,color:#fff style Strong fill:#27ae60,color:#fff style Negotiate fill:#f39c12,color:#fff
离线可用] --> Strong[强缓存 immutable
JS / CSS / 图片 / 字体] Strong --> Negotiate[协商缓存 ETag
HTML 入口文件] end style SW fill:#3498db,color:#fff style Strong fill:#27ae60,color:#fff style Negotiate fill:#f39c12,color:#fff
| 指标 | 优化前 | 优化后 | 变化 |
|---|---|---|---|
| 二次加载时间 | 1.8s | 120ms | 减少了 93% |
| CDN 带宽 | 25.6MB/天/人 | 3.8MB/天/人 | 减少了 85% |
17. 字体子集化 + font-display
分类:字体优化 / 内容可见性
优化前
- 中文全量字体包 8.5MB
- FOIT:文字空白期 4.2s,用户以为页面崩溃
css
@font-face {
font-family: 'CustomFont';
src: url('/fonts/custom-font-full.ttf') format('truetype');
/* 未指定 font-display,默认阻塞渲染 */
}
优化后
- font-spider 子集化 + WOFF2 = 180KB
- font-display: swap,文字空白 0s
css
@font-face {
font-family: 'CustomFont';
src: url('/fonts/custom-font-subset.woff2') format('woff2');
font-display: swap;
unicode-range: U+4E00-9FFF, U+0020-007E;
}
对比图
xychart-beta
title "字体文件体积 (KB)"
x-axis ["优化前 (全量TTF)", "优化后 (子集WOFF2)"]
y-axis "体积 (KB)" 0 --> 9000
bar [8500, 180]
gantt
title 文字可见性时间线
dateFormat X
axisFormat %s
section 优化前 FOIT
文字不可见 :crit, 0, 42
字体加载完才可见 :active, 42, 50
section 优化后 FOUT
系统字体立即可见 → 自定义字体无缝切换 :active, 0, 50
| 指标 | 优化前 | 优化后 | 变化 |
|---|---|---|---|
| 字体体积 | 8.5MB | 180KB | 减少了 98% |
| 文字空白期 | 4.2s | 0s | 减少了 100% |
18. 骨架屏消除白屏感知
分类:用户体验
优化前
- 数据加载期间返回 null --- 完全白屏
- FCP 2.5s
- 35% 用户在白屏期重复刷新
tsx
if (isLoading) return null; // 白屏
优化后
- 骨架屏即时渲染,模拟真实页面布局
- FCP 300ms
- 重复刷新率降至 4%
tsx
if (isLoading) return <DashboardSkeleton />;
css
.skeleton-line {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
对比图
graph LR
subgraph 优化前
A[页面加载] --> B[白屏 2.5s]
B --> C[内容展示]
end
subgraph 优化后
D[页面加载] --> E[骨架屏 300ms]
E --> F[内容展示]
end
style B fill:#e74c3c,color:#fff
style E fill:#27ae60,color:#fff
style C fill:#3498db,color:#fff
style F fill:#3498db,color:#fff
xychart-beta
title "FCP 时间对比 (ms)"
x-axis ["优化前 (白屏)", "优化后 (骨架屏)"]
y-axis "FCP (ms)" 0 --> 2800
bar [2500, 300]
| 指标 | 优化前 | 优化后 | 变化 |
|---|---|---|---|
| FCP | 2.5s | 300ms | 减少了 88% |
| 重复刷新率 | 35% | 4% | 减少了 89% |
| 页面留存率 | --- | --- | 提升了 25% |
19. 流式 Excel 导出
分类:文件处理
优化前
- 10万行 × 30列 在内存中一次性构建 WorkBook
- 内存峰值 1.2GB,频繁触发 Tab 崩溃
- 主线程阻塞 12s ,导出成功率 40%
tsx
const ws = XLSX.utils.json_to_sheet(data); // 10 万行一次性转换
XLSX.writeFile(wb, 'report.xlsx'); // 12 秒阻塞
优化后
- exceljs 流式写入 + Web Worker
- 内存峰值 85MB ,主线程 0ms 阻塞
- 导出成功率 99.8%
tsx
// Web Worker 中逐行写入
const sheet = workbook.addWorksheet('Report');
for (let i = 0; i < data.length; i++) {
sheet.addRow(data[i]).commit(); // 逐行写入并释放内存
}
对比图
xychart-beta
title "内存峰值对比 (MB)"
x-axis ["优化前 (全量构建)", "优化后 (流式写入)"]
y-axis "内存 (MB)" 0 --> 1300
bar [1200, 85]
xychart-beta
title "导出成功率 (%)"
x-axis ["优化前", "优化后"]
y-axis "成功率 (%)" 0 --> 105
bar [40, 99]
| 指标 | 优化前 | 优化后 | 变化 |
|---|---|---|---|
| 内存峰值 | 1.2GB | 85MB | 减少了 93% |
| 主线程阻塞 | 12s | 0ms | 减少了 100% |
| 导出成功率 | 40% | 99.8% | 提升了 150% |
20. 首屏关键 CSS 内联
分类:CSS / 首屏渲染
优化前
- 320KB 全量 CSS 阻塞渲染
- CSS 下载耗时 800ms
- FCP 1.6s
html
<link rel="stylesheet" href="/styles/main.css" /> <!-- 320KB 阻塞 -->
优化后
- 12KB 关键 CSS 内联到
<style>,非关键 CSS 异步加载 - FCP 400ms
html
<style>/* 首屏关键样式 12KB */</style>
<link rel="preload" href="/styles/main.css" as="style" onload="this.rel='stylesheet'" />
对比图
gantt
title 首屏渲染时间线
dateFormat X
axisFormat %s
section 优化前
HTML 解析 :active, 0, 3
CSS 下载 320KB 阻塞 :crit, 3, 11
FCP @ 1.6s :milestone, 11, 11
section 优化后
HTML + 内联 CSS 解析 :active, 0, 4
FCP @ 400ms :milestone, 4, 4
CSS 异步加载 :done, 4, 11
| 指标 | 优化前 | 优化后 | 变化 |
|---|---|---|---|
| FCP | 1.6s | 400ms | 减少了 75% |
21. IntersectionObserver 按需渲染
分类:JavaScript / 滚动性能
优化前
- scroll 事件 + getBoundingClientRect 判断可见性
- 每秒 30-60 次回调,强制同步布局
- CPU 45% ,帧率 35fps
- 15 个图表一次性初始化 2.8s
tsx
window.addEventListener('scroll', () => {
elements.forEach(el => {
const rect = el.getBoundingClientRect(); // 每次触发 Layout
});
});
优化后
- IntersectionObserver 浏览器原生异步通知
- CPU 3% ,帧率 60fps
- 首屏仅初始化 2 个图表 0.4s
tsx
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.unobserve(entry.target);
}
}, { rootMargin: '200px' });
observer.observe(el);
对比图
xychart-beta
title "滚动时 CPU 占用率 (%)"
x-axis ["优化前 (scroll)", "优化后 (IO)"]
y-axis "CPU (%)" 0 --> 50
bar [45, 3]
xychart-beta
title "首屏图表初始化耗时 (ms)"
x-axis ["优化前 (15个全部)", "优化后 (2个按需)"]
y-axis "耗时 (ms)" 0 --> 3000
bar [2800, 400]
| 指标 | 优化前 | 优化后 | 变化 |
|---|---|---|---|
| CPU 占用 | 45% | 3% | 减少了 93% |
| 首屏初始化 | 2.8s | 0.4s | 减少了 86% |
| 滚动帧率 | 35fps | 60fps | 提升了 71% |
22. WebSocket 替代轮询
分类:网络通信 / 实时性
优化前
- 3 秒间隔轮询,每次返回全量 45KB
- 200 人在线 → 服务端 4000 QPS/min
- 95% 响应数据无变化 --- 纯浪费
- 数据延迟最大 3 秒
tsx
const timer = setInterval(async () => {
const res = await fetch('/api/tickets');
setTickets(await res.json());
}, 3000);
优化后
- WebSocket 长连接,服务端仅在数据变更时推送增量
- 服务端 50 推送/min
- 传输数据量 减少 97%
- 数据延迟 毫秒级
tsx
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'update') {
setTickets(prev => {
const next = new Map(prev);
next.set(msg.data.id, { ...next.get(msg.data.id)!, ...msg.data });
return next;
});
}
};
对比图
xychart-beta
title "服务端 QPS/min (200人在线)"
x-axis ["优化前 (轮询)", "优化后 (WebSocket)"]
y-axis "QPS / min" 0 --> 4500
bar [4000, 50]
gantt
title 数据更新时间线 (15秒窗口, 第7秒发生变更)
dateFormat X
axisFormat %s
section 优化前 - 轮询
无变更请求 :crit, 0, 3
无变更请求 :crit, 3, 6
无变更请求 :crit, 6, 9
命中变更 :active, 9, 12
无变更请求 :crit, 12, 15
section 优化后 - WebSocket
持久连接保持中 :done, 0, 15
变更即时推送 @ 7s :active, 7, 8
| 指标 | 优化前 | 优化后 | 变化 |
|---|---|---|---|
| 服务端 QPS | 4,000/min | 50/min | 减少了 99% |
| 传输数据量 | 900KB/min | 27KB/min | 减少了 97% |
| 数据延迟 | 最大 3s | 毫秒级 | 减少了 99% |
全部 22 条优化总览
| # | 优化手段 | 分类 | 核心指标 | 优化前 | 优化后 | 提升幅度 |
|---|---|---|---|---|---|---|
| 1 | React.memo | React | 渲染耗时 | 120ms | 8ms | 减少了 93% |
| 2 | 虚拟滚动 | DOM | DOM 节点 | 15,000+ | 60 | 减少了 99.6% |
| 3 | 代码分割 | 构建 | 首屏 JS | 860KB | 210KB | 减少了 76% |
| 4 | 防抖+缓存 | 网络 | 请求数 | 11 次 | 1-2 次 | 减少了 90% |
| 5 | WebP+懒加载 | 资源 | 图片体积 | 7MB | 1.7MB | 减少了 76% |
| 6 | useMemo | React | 计算耗时 | 65ms | 0.1ms | 减少了 99% |
| 7 | Tree Shaking | 构建 | 依赖体积 | 530KB | 28KB | 减少了 95% |
| 8 | 状态下沉 | React | 渲染组件数 | 47 | 3 | 减少了 94% |
| 9 | transform 动画 | CSS | 帧率 | 24fps | 60fps | 提升了 150% |
| 10 | 事件委托 | JS | 监听器数 | 1,500 | 1 | 减少了 99.9% |
| 11 | Web Worker | JS | 主线程阻塞 | 3.8s | 0ms | 减少了 100% |
| 12 | 请求合并 | 网络 | 请求数 | 8 | 1 | 减少了 87.5% |
| 13 | 分片上传 | 上传 | 成功率 | 62% | 99.5% | 提升了 60% |
| 14 | 客户端压缩 | 上传 | 上传体积 | 50MB | 3MB | 减少了 94% |
| 15 | DNS 预连接 | 网络 | 连接延迟 | 280ms | ~0ms | 减少了 100% |
| 16 | 缓存分层 | 缓存 | 二次加载 | 1.8s | 120ms | 减少了 93% |
| 17 | 字体子集化 | 字体 | 字体体积 | 8.5MB | 180KB | 减少了 98% |
| 18 | 骨架屏 | UX | FCP | 2.5s | 300ms | 减少了 88% |
| 19 | 流式导出 | 文件 | 内存峰值 | 1.2GB | 85MB | 减少了 93% |
| 20 | 关键CSS内联 | CSS | FCP | 1.6s | 400ms | 减少了 75% |
| 21 | IntersectionObserver | JS | CPU 占用 | 45% | 3% | 减少了 93% |
| 22 | WebSocket | 网络 | QPS/min | 4,000 | 50 | 减少了 99% |