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 天前
Vue的响应式原理
掘金·金石计划
做前端的娜娜子2 天前
Web缓存策略有哪些?
掘金·金石计划
光影少年10 天前
RN 原生动画 Animated 用法、动画性能优化
react native·react.js·掘金·金石计划
光影少年13 天前
RN适配方案:屏幕适配、分辨率适配、刘海屏/全面屏适配
react native·react.js·掘金·金石计划
光影少年13 天前
RN中的StyleSheet.create 好处、内联样式弊端
react native·react.js·掘金·金石计划
光影少年15 天前
RN 样式特点:Flex 布局默认、没有像素单位、样式不能级联
react native·react.js·掘金·金石计划
光影少年22 天前
react navite JSBridge 通信机制、异步通信特点
react native·react.js·掘金·金石计划