前端性能调优实战指南 — 22 条优化策略

20 阅读14分钟

前端性能调优实战指南 — 22 条优化策略


1. React.memo 避免无效重渲染

分类:React / 渲染优化

优化前

  • 父组件 state 变化,200 个子组件全部重渲染
  • Profiler 单次渲染耗时 120ms
  • 搜索输入有明显卡顿
// 每次父组件 re-render,RowItem 都会重新渲染
function RowItem({ data }: { data: RowData }) {
  return <div>{data.name} - {data.value}</div>;
}

优化后

  • React.memo 包裹子组件,props 不变则跳过渲染
  • 渲染耗时降至 8ms
  • 搜索输入丝滑流畅
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]
指标优化前优化后变化
渲染耗时120ms8ms减少了 93%
重渲染组件数200 个5 个减少了 96%

2. 虚拟滚动长列表

分类:DOM 优化

优化前

  • 5000+ 行数据全量 DOM 渲染
  • DOM 节点数 15,000+
  • 首屏渲染 3.2s,滚动仅 12fps
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
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.2s200ms减少了 94%
滚动帧率12fps60fps提升了 5 倍

3. 路由级代码分割

分类:构建优化

优化前

  • 所有页面打包为单个 main.js
  • 首屏 JS 体积 860KB (gzip)
  • 3G 网络首屏白屏 4.5s
import HomePage from './pages/home';
import DashboardPage from './pages/dashboard';
import SettingsPage from './pages/settings';

优化后

  • React.lazy 按路由动态导入,按需加载
  • 首屏 JS 体积 210KB
  • 首屏加载 1.8s
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
指标优化前优化后变化
首屏 JS860KB210KB减少了 76%
首屏加载时间4.5s1.8s减少了 60%

4. 接口防抖 + 缓存

分类:网络请求 / 缓存策略

优化前

  • 每次键入字符立即请求 API
  • 输入 11 字符触发 11 次请求
  • 服务端峰值 800 QPS/min
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
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%
服务端 QPS800/min80/min减少了 90%
缓存命中延迟200ms0ms减少了 100%

5. 图片 WebP + 懒加载

分类:资源优化

优化前

  • 20 张 PNG Banner,均值 350KB/张
  • 总体积 7MB,首屏一次性加载
  • LCP 5.8s
<img src="/images/banner1.png" />
<!-- 20 张全部在首屏加载 -->

优化后

  • WebP 格式 + loading="lazy" 原生懒加载
  • 均值 85KB/张,首屏仅加载 3 张
  • LCP 2.1s
<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
指标优化前优化后变化
图片体积7MB1.7MB减少了 76%
LCP5.8s2.1s减少了 64%

6. useMemo 缓存昂贵计算

分类:React / 计算优化

优化前

  • 10000 条数据每次渲染都重新过滤、分组、聚合
  • 计算耗时 65ms
  • 切换 Tab 有明显延迟
function Dashboard({ rawData, activeTab }: Props) {
  const chartData = processRawData(rawData);   // 每次渲染都执行 45ms
  const summary = calculateSummary(rawData);    // 每次渲染都执行 20ms
}

优化后

  • useMemo 缓存,仅在 rawData 变化时重新计算
  • 缓存命中耗时 0.1ms
  • 切换 Tab 即时响应
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]
指标优化前优化后变化
计算耗时65ms0.1ms减少了 99%

7. Tree Shaking 按需导入

分类:构建优化 / 依赖瘦身

优化前

  • lodash 全量导入 + moment.js
  • 仅用 3 个函数,却引入 530KB
  • 占总 bundle 35%
import _ from 'lodash';
import moment from 'moment';

优化后

  • lodash 路径导入 + dayjs 替代 moment
  • 两库合计 28KB
  • 占总 bundle 2.5%
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
指标优化前优化后变化
依赖体积530KB28KB减少了 95%
占 bundle 比例35%2.5%减少了 93%

8. 状态管理下沉

分类:React / 状态管理

优化前

  • 表单每个字段的临时值都存 Redux 全局 store
  • 输入触发 47 个 组件重渲染(含导航栏、侧边栏)
  • 输入延迟 80ms,打字卡顿
const value = useSelector((state) => state.form[fieldName]);
const dispatch = useDispatch();
<input value={value} onChange={e => dispatch(updateFormField(fieldName, e.target.value))} />

优化后

  • 表单状态下沉到局部 useState,提交时才同步到全局
  • 3 个 组件重渲染
  • 输入延迟 4ms
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]
指标优化前优化后变化
渲染组件数473减少了 94%
输入延迟80ms4ms减少了 95%

9. CSS transform 替代位置属性动画

分类:CSS / 动画流畅度

优化前

  • 抽屉展开/收起使用 left 属性做动画
  • 每帧触发 Layout → Paint → Composite(三阶段全走)
  • 帧率 24fps,CPU 65%
.drawer {
  left: -400px;
  transition: left 0.3s ease;
  &.open { left: 0; } // 触发 Layout 重排
}

优化后

  • 改用 transform: translateX(),仅触发 Composite
  • 帧率 60fps,CPU 12%
.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]
指标优化前优化后变化
帧率24fps60fps提升了 150%
CPU 占用65%12%减少了 82%

10. 事件委托替代逐项绑定

分类:JavaScript / 内存优化

优化前

  • 500 行 × 3 按钮 = 1500 个事件监听器
  • 初始化耗时 90ms,内存多占 12MB
{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
<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,5001减少了 99.9%
初始化耗时90ms0.5ms减少了 99%
内存占用12MB~1MB减少了 92%

11. Web Worker 卸载 CPU 密集型任务

分类:JavaScript / 用户体验

优化前

  • 50000 行 Excel 在主线程解析、校验、转换
  • 主线程阻塞 3.8s,页面完全冻结
const handleFileUpload = async (file: File) => {
  const rawData = await readExcelFile(file);       // 800ms
  const validated = validateRows(rawData);           // 1500ms
  const transformed = transformData(validated);      // 1500ms — 期间页面冻结
};

优化后

  • 计算逻辑移至 Web Worker,主线程始终可交互
  • 主线程阻塞 0ms,有实时进度条
// 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.8s0ms减少了 100%
处理速度3.8s3.2s提升了 15%

12. HTTP 请求合并

分类:网络请求 / 首屏体验

优化前

  • 看板初始化 8 个独立 API,受浏览器并发限制分 2 批
  • 瀑布流总耗时 1200ms
  • HTTP 头冗余 16KB
const [sales, orders, users, ...] = await Promise.all([
  fetch('/api/dashboard/sales'),
  fetch('/api/dashboard/orders'),
  // ...共 8 个请求
]);

优化后

  • 后端提供批量接口,1 次请求返回全部数据
  • 总耗时 350ms,头开销 2KB
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
指标优化前优化后变化
请求数81减少了 87.5%
加载时间1200ms350ms减少了 71%

13. 大文件分片上传

分类:文件上传 / 网络优化

优化前

  • 500MB 视频文件单次 POST 直传
  • 上传成功率仅 62%
  • 断网后需从头重传全部 500MB
  • 超 100MB 触发 Nginx 413 错误
const formData = new FormData();
formData.append('file', file); // 500MB 一次性发送
await fetch('/api/upload', { method: 'POST', body: formData });

优化后

  • 5MB 分片 + 3 路并发 + 断点续传
  • 上传成功率 99.5%
  • 断网恢复后只续传剩余分片
  • 上传耗时从 180s 降至 72s
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%
上传耗时180s72s减少了 60%
断网重传量100%~5%减少了 95%

14. 客户端图片压缩

分类:文件上传 / Canvas API

优化前

  • 10 张手机照片原图直传(4032×3024)
  • 均值 5MB/张,总计 50MB
  • 4G 网络上传耗时 45s
Array.from(files).forEach(file => {
  formData.append('images', file); // 每张 4-6MB 原图
});

优化后

  • Canvas 压缩 + 缩放到 1920px + quality 0.8
  • 均值 300KB/张,总计 3MB
  • 上传耗时 3s
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]
指标优化前优化后变化
上传体积50MB3MB减少了 94%
上传时间45s3s减少了 93%
存储成本50MB/批3MB/批减少了 94%

15. DNS 预解析 + 预连接

分类:网络优化 / 首屏体验

优化前

  • 首次请求需完整建立连接
  • DNS(80ms) + TCP(80ms) + TLS(120ms) = 280ms
  • 5 个域名累计延迟 1.4s
<!-- 无任何预处理,连接在使用时才建立 -->
<link rel="stylesheet" href="https://cdn.example.com/styles/main.css" />

优化后

  • dns-prefetch + preconnect 提前建立连接
  • 首次请求连接延迟 ~0ms
<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%
# 静态资源强缓存
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<br/>离线可用] --> Strong[强缓存 immutable<br/>JS / CSS / 图片 / 字体]
    Strong --> Negotiate[协商缓存 ETag<br/>HTML 入口文件]
    end
    style SW fill:#3498db,color:#fff
    style Strong fill:#27ae60,color:#fff
    style Negotiate fill:#f39c12,color:#fff
指标优化前优化后变化
二次加载时间1.8s120ms减少了 93%
CDN 带宽25.6MB/天/人3.8MB/天/人减少了 85%

17. 字体子集化 + font-display

分类:字体优化 / 内容可见性

优化前

  • 中文全量字体包 8.5MB
  • FOIT:文字空白期 4.2s,用户以为页面崩溃
@font-face {
  font-family: 'CustomFont';
  src: url('/fonts/custom-font-full.ttf') format('truetype');
  /* 未指定 font-display,默认阻塞渲染 */
}

优化后

  • font-spider 子集化 + WOFF2 = 180KB
  • font-display: swap,文字空白 0s
@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.5MB180KB减少了 98%
文字空白期4.2s0s减少了 100%

18. 骨架屏消除白屏感知

分类:用户体验

优化前

  • 数据加载期间返回 null — 完全白屏
  • FCP 2.5s
  • 35% 用户在白屏期重复刷新
if (isLoading) return null; // 白屏

优化后

  • 骨架屏即时渲染,模拟真实页面布局
  • FCP 300ms
  • 重复刷新率降至 4%
if (isLoading) return <DashboardSkeleton />;
.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]
指标优化前优化后变化
FCP2.5s300ms减少了 88%
重复刷新率35%4%减少了 89%
页面留存率提升了 25%

19. 流式 Excel 导出

分类:文件处理

优化前

  • 10万行 × 30列 在内存中一次性构建 WorkBook
  • 内存峰值 1.2GB,频繁触发 Tab 崩溃
  • 主线程阻塞 12s,导出成功率 40%
const ws = XLSX.utils.json_to_sheet(data); // 10 万行一次性转换
XLSX.writeFile(wb, 'report.xlsx');          // 12 秒阻塞

优化后

  • exceljs 流式写入 + Web Worker
  • 内存峰值 85MB,主线程 0ms 阻塞
  • 导出成功率 99.8%
// 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.2GB85MB减少了 93%
主线程阻塞12s0ms减少了 100%
导出成功率40%99.8%提升了 150%

20. 首屏关键 CSS 内联

分类:CSS / 首屏渲染

优化前

  • 320KB 全量 CSS 阻塞渲染
  • CSS 下载耗时 800ms
  • FCP 1.6s
<link rel="stylesheet" href="/styles/main.css" /> <!-- 320KB 阻塞 -->

优化后

  • 12KB 关键 CSS 内联到 <style>,非关键 CSS 异步加载
  • FCP 400ms
<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
指标优化前优化后变化
FCP1.6s400ms减少了 75%

21. IntersectionObserver 按需渲染

分类:JavaScript / 滚动性能

优化前

  • scroll 事件 + getBoundingClientRect 判断可见性
  • 每秒 30-60 次回调,强制同步布局
  • CPU 45%,帧率 35fps
  • 15 个图表一次性初始化 2.8s
window.addEventListener('scroll', () => {
  elements.forEach(el => {
    const rect = el.getBoundingClientRect(); // 每次触发 Layout
  });
});

优化后

  • IntersectionObserver 浏览器原生异步通知
  • CPU 3%,帧率 60fps
  • 首屏仅初始化 2 个图表 0.4s
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.8s0.4s减少了 86%
滚动帧率35fps60fps提升了 71%

22. WebSocket 替代轮询

分类:网络通信 / 实时性

优化前

  • 3 秒间隔轮询,每次返回全量 45KB
  • 200 人在线 → 服务端 4000 QPS/min
  • 95% 响应数据无变化 — 纯浪费
  • 数据延迟最大 3 秒
const timer = setInterval(async () => {
  const res = await fetch('/api/tickets');
  setTickets(await res.json());
}, 3000);

优化后

  • WebSocket 长连接,服务端仅在数据变更时推送增量
  • 服务端 50 推送/min
  • 传输数据量 减少 97%
  • 数据延迟 毫秒级
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
指标优化前优化后变化
服务端 QPS4,000/min50/min减少了 99%
传输数据量900KB/min27KB/min减少了 97%
数据延迟最大 3s毫秒级减少了 99%

全部 22 条优化总览

#优化手段分类核心指标优化前优化后提升幅度
1React.memoReact渲染耗时120ms8ms减少了 93%
2虚拟滚动DOMDOM 节点15,000+60减少了 99.6%
3代码分割构建首屏 JS860KB210KB减少了 76%
4防抖+缓存网络请求数11 次1-2 次减少了 90%
5WebP+懒加载资源图片体积7MB1.7MB减少了 76%
6useMemoReact计算耗时65ms0.1ms减少了 99%
7Tree Shaking构建依赖体积530KB28KB减少了 95%
8状态下沉React渲染组件数473减少了 94%
9transform 动画CSS帧率24fps60fps提升了 150%
10事件委托JS监听器数1,5001减少了 99.9%
11Web WorkerJS主线程阻塞3.8s0ms减少了 100%
12请求合并网络请求数81减少了 87.5%
13分片上传上传成功率62%99.5%提升了 60%
14客户端压缩上传上传体积50MB3MB减少了 94%
15DNS 预连接网络连接延迟280ms~0ms减少了 100%
16缓存分层缓存二次加载1.8s120ms减少了 93%
17字体子集化字体字体体积8.5MB180KB减少了 98%
18骨架屏UXFCP2.5s300ms减少了 88%
19流式导出文件内存峰值1.2GB85MB减少了 93%
20关键CSS内联CSSFCP1.6s400ms减少了 75%
21IntersectionObserverJSCPU 占用45%3%减少了 93%
22WebSocket网络QPS/min4,00050减少了 99%