React 性能优化

摘要 :React 性能优化的核心目标是减少不必要的 re-render(重渲染)降低首屏/交互耗时 。优化手段分三层:组件层(memo/PureComponent/useMemo/useCallback 控制更新粒度)、加载层(lazy/Suspense 分 chunk + 虚拟列表 + Web Worker)和工程层(打包构建/资源压缩/缓存策略)。一句话概括------能不渲染就不渲染,能晚加载就晚加载,能放后台就放后台。

一、为什么需要性能优化?

React 默认的渲染机制:父组件 state 变化 → 所有子组件都会重新执行 render 函数,即使子组件的 props 没有变化。

jsx 复制代码
// 父组件状态变了 → ChildA 和 ChildB 都会重新 render
function App() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
      <ChildA />          {/* count 变了,ChildA 也重新 render */}
      <ChildB data="固定值" /> {/* props 没变,但仍然重新 render */}
    </div>
  );
}

当组件树变大时,无意义的 re-render 会造成明显的卡顿。优化的本质就是精准控制"谁该重新渲染"


二、减少不必要的 Re-render

2.1 React.memo(函数组件)& PureComponent(类组件)

两者都是通过浅比较(shallow compare) props 来决定是否跳过渲染。

jsx 复制代码
// 函数组件:用 React.memo 包裹
const ExpensiveComponent = React.memo(function ({ data }) {
  console.log('ExpensiveComponent 渲染了');
  return <div>{data.name}</div>;
});

// 只有 data 引用变了才会重新渲染
// data.name 改了但引用没变?不会触发!注意这个坑
jsx 复制代码
// 类组件:继承 React.PureComponent
class ExpensiveList extends React.PureComponent {
  render() {
    console.log('ExpensiveList 渲染了');
    return <ul>{this.props.items.map(...)}</ul>;
  }
}
// 等价于 shouldComponentUpdate 做浅比较

2.2 useMemo --- 缓存计算结果

避免每次 render 都重复执行昂贵的计算。

jsx 复制代码
function FilteredList({ items, filter }) {
  // ❌ 每次 render 都重新过滤
  const filtered = items.filter(item => item.includes(filter));

  // ✅ 只有 items 或 filter 变了才重新计算
  const filtered = useMemo(
    () => items.filter(item => item.includes(filter)),
    [items, filter]  // 依赖数组
  );

  return <ul>{filtered.map(...)}</ul>;
}

2.3 useCallback --- 缓存函数引用

防止因父组件 re-render 导致子组件收到新的函数引用,从而破坏 memo 的效果。

jsx 复制代码
function Parent({ count }) {
  // ❌ 每次 render 都创建新函数 → 子组件 memo 失效
  const handleClick = () => console.log(count);

  // ✅ count 不变时返回同一个函数引用
  const handleClick = useCallback(
    () => console.log(count),
    [count]
  );

  return <Child onClick={handleClick} />;
}

const Child = React.memo(({ onClick }) => (
  <button onClick={onClick}>click</button>
));

2.4 不可变数据结构 & Immer

React 的浅比较要求数据引用不变才视为"没改"。直接修改对象/数组会绕过检测。

js 复制代码
// ❌ 直接修改 ------ 引用没变,React 检测不到变化
state.items.push(newItem);
setState(state);  // 不会触发 re-render!

// ✅ 创建新引用
setState({
  ...state,
  items: [...state.items, newItem]
});

Immer 用 Proxy 拦截写操作,让你用"直接修改"的语法写出不可变更新:

js 复制代码
import { produce } from 'immer';

const nextState = produce(state, draft => {
  draft.items.push(newItem);   // 看起来像直接修改
  draft.user.name = 'newName'; // 实际 Immer 生成了新对象
});
setState(nextState);

三、按需加载与代码分割

3.1 React.lazy + Suspense

把不需要首屏立即展示的组件拆成独立 chunk,用到时再加载。

jsx 复制代码
import { lazy, Suspense } from 'react';

// 动态 import → webpack 自动拆分成独立 chunk
const HeavyChart = lazy(() => import('./HeavyChart'));
const AdminPanel = lazy(() => import('./AdminPanel'));

function App() {
  return (
    <Suspense fallback={<div>加载中...</div>}>
      {isAdmin && <AdminPanel />}
      <HeavyChart />
    </Suspense>
  );
}

效果 :首屏 JS 体积大幅减小,HeavyChartAdminPanel 的代码在需要时才下载执行。

3.2 虚拟列表(大列表优化)

渲染 10000 条 DOM 必然卡顿。虚拟列表只渲染可视区域内的几十条元素。

jsx 复制代码
// react-window 或 react-virtualized
import { FixedSizeList as List } from 'react-window';

function VirtualList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style}>{items[index].name}</div>
  );

  return (
    <List
      height={500}        // 容器高度
      itemCount={10000}   // 总条数
      itemSize={50}       // 每行高度
    >
      {Row}
    </List>
  );
}
// 无论 10000 条还是 100 万条,DOM 节点始终只有 ~11 个(500/50 + 缓冲)

四、Web Worker --- 把重计算移出主线程

JS 是单线程的,复杂计算会阻塞 UI 渲染。Web Worker 在独立线程中运行,不阻塞主线程。

js 复制代码
// worker.js
self.onmessage = function(e) {
  const { data } = e.data;
  // 耗时计算:大数据排序、图像处理、复杂算法
  const result = heavyComputation(data);
  self.postMessage(result);
};

// 组件中使用
function DataProcessor({ rawData }) {
  const [result, setResult] = useState(null);

  useEffect(() => {
    const worker = new Worker('./worker.js');

    worker.onmessage = (e) => {
      setResult(e.data);  // 计算完成,拿到结果
      worker.terminate();
    };

    worker.postMessage({ data: rawData });  // 发送数据给 worker

    return () => worker.terminate();
  }, [rawData]);

  return result ? <ResultView data={result} /> : <Spinner />;
}

五、状态管理优化

方案 适用场景 优化要点
useState / useReducer 组件内或简单跨组件 拆分 state,避免无关 state 变化触发大面积 re-render
Context + useReducer 中小型应用全局状态 拆分多个 Context,不要把所有状态塞进一个 Provider
Redux / Zustand / Jotai 大型应用 用 selector 精准订阅,只监听需要的 slice
外部状态库(SWR/React Query) 服务端数据 内置缓存、去重、自动重请求

关键原则------Context 的粒度要细

jsx 复制代码
// ❌ 一个大 Context → 任何状态变化都导致所有消费者 re-render
const AppContext = createContext({ user, theme, locale, cart });

// ✅ 拆成多个小 Context → 各管各的
const UserContext = createContext(user);
const ThemeContext = createContext(theme);

// 只消费 theme 的组件不会因为 user 变化而 re-render
function ThemedButton() {
  const theme = useContext(ThemeContext);
  return <button style={{ color: theme.primary }}>click</button>;
}

六、工程层优化全景

你的手绘笔记覆盖了从构建到首屏的全链路:

层级 手段 效果
打包构建 Terser 压缩、Code Splitting(chunk)、Tree Shaking、构建缓存 减少包体积、加快构建速度
应用层面 CSS transform 替代 top/left(避免 reflow)、Web Worker 减少主线程阻塞
资源加载 Gzip/Brotli 压缩、字体子集化(font-spider)、CDN/OSS、HTTP/2 多路复用 加速资源传输
缓存策略 强缓存(Cache-Control)、协商缓存(ETag)、策略缓存(Service Worker) 减少重复请求
首屏资源 Next.js SSR/SSG、<link rel="prefetch">、首屏关键路径优先 加快 FCP/LCP

七、完整流程串联:一次 re-render 的生命周期

scss 复制代码
用户操作 / 定时器 / 网络响应
       │
       ▼
  setState / dispatch
       │
       ▼
  ┌─────────────────────┐
  │  1. 标记组件为 dirty │
  └──────────┬──────────┘
             │
             ▼
  ┌─────────────────────┐     未命中 memo
  │  2. shouldComponentUpdate│ ──→ 执行 render
  │     或 React.memo 比较   │
  └──────────┬──────────┘
             │ 命中 memo(props 浅比较相等)
             ▼
         ⏭️ 跳过 render ✅
             │
             ▼ (未命中)
  ┌─────────────────────┐
  │  3. 执行 render 函数  │ ← 这里调用 useMemo/useCallback
  │     生成新的 VNode    │
  └──────────┬──────────┘
             │
             ▼
  ┌─────────────────────┐
  │  4. Reconciliation   │ ← Diff 算法对比新旧 VNode
  │     (Diffing)        │
  └──────────┬──────────┘
             │
             ▼
  ┌─────────────────────┐
  │  5. 提交变更到 DOM    │ ← commitRoot,批量更新
  │     (Commit)         │
  └──────────┬──────────┘
             │
             ▼
       浏览器绘制 (Paint)

优化插入点

  • 第 2 步 → React.memo / PureComponent / shouldComponentUpdate
  • 第 3 步 → useMemo 缓存计算、useCallback 缓存函数
  • 第 4 步 → key 属性帮助 Diff 复用节点、虚拟列表减少节点数
  • 第 5 步 → CSS transform 触发 GPU 合成层,减少 layout/paint

八、各方案选型对比

场景 推荐方案 不推荐
纯展示组件,props 很少变 React.memo 无脑全量 re-render
传递给子组件的回调函数 useCallback + memo 每次render创建新函数
渲染前需要昂贵计算 useMemo 直接在 render 里算
大数据列表(1000+ 条) 虚拟列表(react-window) 全部渲染
页面中有重型非首屏组件 React.lazy + Suspense 同步 import
复杂计算/数据处理 Web Worker 主线程同步跑
全局状态频繁更新 拆分多个 Context / Zustand 单一大 Context
对象/数组深层更新 Immer 手动展开运算符(易出错)

九、记忆口诀

  • memo 控入口:React.memo 守大门,props 没变别进来
  • Memo 算得快:useMemo 存结果,useCallback 存函数
  • 懒加载减体积:lazy + Suspense,chunk 按需下
  • 大表虚拟化:一万条只画十条,react-window 来帮忙
  • Worker 搬后台:重活丢给 Worker,主线程不卡顿
  • 一句话总结能 memo 就 memo,能拆就拆,能懒就懒,能搬就搬
相关推荐
做前端的娜娜子1 天前
文本切片与召回(Chunk、Overlap 到混合检索)学习笔记
langchain·openai·掘金·金石计划
做前端的娜娜子1 天前
#浏览器存储方案:localStorage、sessionStorage 与 Cookie
前端·面试·掘金·金石计划
光影少年2 天前
react navite性能优化 & 常见坑
前端·react native·掘金·金石计划
做前端的娜娜子5 天前
文档加载工程:多格式数据接入与 Document 标准化
langchain·ai编程·掘金·金石计划
做前端的娜娜子6 天前
Embedding 向量模型:从语义表示到相似度计算
langchain·openai·掘金·金石计划
做前端的娜娜子6 天前
第二个 AI 调用——invoke 阻塞式与 stream 流式生成
人工智能·llm·掘金·金石计划
做前端的娜娜子7 天前
第一个AI调用——TypeScript 工程初始化与配置管理(实战跟练)
langchain·openai·掘金·金石计划
做前端的娜娜子9 天前
什么是浅拷贝(Shallow Copy)和深拷贝(Deep Copy)?如何手动实现一个深拷贝函数
javascript·面试·掘金·金石计划
做前端的娜娜子10 天前
JavaScript 闭包
前端·javascript·掘金·金石计划
做前端的娜娜子11 天前
移动端上拉加载与下拉刷新实现方案
前端·面试·掘金·金石计划