React 性能优化组件设计模式与通信

一、React 为什么会"慢"?

React 默认行为:父组件重新渲染 → 所有子组件都跟着重新渲染,不管子组件的 props 有没有变。

复制代码
App 重新渲染
 ├── Header 重新渲染 ← props 没变也渲染了(浪费)
 ├── Content 重新渲染 ← 确实需要更新
 └── Footer 重新渲染 ← props 没变也渲染了(浪费)

性能优化的核心:减少不必要的渲染和计算

二、优化手段

1. React.memo --- 跳过不必要的子组件渲染

用 memo 包裹子组件,props 不变就不重新渲染。

jsx 复制代码
import { memo } from 'react';

// 没有 memo:父组件每次渲染,Header 都跟着渲染
// 有 memo:只有 title 变了才重新渲染
const Header = memo(function Header({ title }) {
  console.log('Header 渲染了');
  return <h1>{title}</h1>;
});

function App() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <Header title="标题" />  {/* count 变了,但 title 没变 → Header 不渲染 */}
      <button onClick={() => setCount(c => c + 1)}>{count}</button>
    </div>
  );
}

memo 失效的常见原因

jsx 复制代码
// ❌ 每次渲染都创建新对象/数组/函数 → 引用变了 → memo 失效
<Child style={{ color: 'red' }} />       // 每次都是新对象
<Child list={[1, 2, 3]} />              // 每次都是新数组
<Child onClick={() => doSomething()} /> // 每次都是新函数

// ✅ 用 useMemo / useCallback 稳定引用
const style = useMemo(() => ({ color: 'red' }), []);
const list = useMemo(() => [1, 2, 3], []);
const handleClick = useCallback(() => doSomething(), []);

2. useMemo --- 缓存计算结果

jsx 复制代码
function ProductList({ products, keyword }) {
  // ❌ 每次渲染都重新过滤(即使 products 和 keyword 没变)
  // const filtered = products.filter(p => p.name.includes(keyword));

  // ✅ 只在依赖变化时才重新计算
  const filtered = useMemo(() => {
    return products.filter(p => p.name.includes(keyword));
  }, [products, keyword]);

  return <ul>{filtered.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}

3. useCallback --- 缓存函数引用

jsx 复制代码
function Parent() {
  const [count, setCount] = useState(0);

  // ✅ 函数引用稳定,配合 memo 的子组件不会重新渲染
  const handleClick = useCallback(() => {
    setCount(c => c + 1);
  }, []);

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

const MemoChild = memo(({ onClick }) => {
  console.log('子组件渲染');
  return <button onClick={onClick}>点击</button>;
});

4. 列表优化 --- 正确使用 key

jsx 复制代码
// ❌ 用 index 做 key(增删时性能差,可能出 bug)
list.map((item, i) => <Item key={i} data={item} />)

// ✅ 用唯一 id 做 key
list.map(item => <Item key={item.id} data={item} />)

5. 懒加载 --- React.lazy + Suspense

按需加载组件,首屏不需要的组件延后加载。

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

// 懒加载:只有用到时才下载这个组件的代码
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));

function App() {
  return (
    <Suspense fallback={<div>加载中...</div>}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}

6. 避免不必要的 state

jsx 复制代码
// ❌ 能从现有 state 算出来的值,不要单独存 state
const [list, setList] = useState([]);
const [count, setCount] = useState(0);  // 多余!

// ✅ 直接计算
const [list, setList] = useState([]);
const count = list.length;  // 不需要额外的 state 和 setState

7. 状态下放 --- 缩小渲染范围

jsx 复制代码
// ❌ 输入框的 state 放在顶层 → 每次输入整个页面都重新渲染
function App() {
  const [text, setText] = useState('');
  return (
    <div>
      <input value={text} onChange={e => setText(e.target.value)} />
      <HeavyComponent />  {/* 每次输入都跟着渲染 */}
    </div>
  );
}

// ✅ 把输入框抽成独立组件 → 只有输入框自己重新渲染
function SearchInput() {
  const [text, setText] = useState('');
  return <input value={text} onChange={e => setText(e.target.value)} />;
}

function App() {
  return (
    <div>
      <SearchInput />
      <HeavyComponent />  {/* 不受输入影响 */}
    </div>
  );
}

8. 虚拟列表 --- 长列表优化

10000 条数据不用全部渲染,只渲染可视区域内的几十条。

bash 复制代码
npm install react-window
jsx 复制代码
import { FixedSizeList } from 'react-window';

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

  return (
    <FixedSizeList
      height={400}       // 可视区域高度
      width="100%"
      itemCount={items.length}  // 总数据量
      itemSize={50}      // 每行高度
    >
      {Row}
    </FixedSizeList>
  );
}
// 10000 条数据,实际只渲染 ~10 个 DOM 节点

三、优化手段速查表

优化方式 解决什么问题 使用场景
React.memo 子组件不必要的重新渲染 props 不常变的子组件
useMemo 重复的昂贵计算 过滤、排序、格式化
useCallback 函数引用变化导致子组件渲染 配合 memo 使用
React.lazy 首屏加载太慢 路由级别懒加载
状态下放 state 放太高导致大范围渲染 局部交互(输入框、弹窗)
虚拟列表 长列表 DOM 太多 数据量 > 几百条
正确的 key 列表 Diff 效率低 所有列表渲染
避免内联对象/函数 memo 失效 传给 memo 组件的 props

四、高频面试题

Q1:React.memo、useMemo、useCallback 的区别?

对比 React.memo useMemo useCallback
作用对象 组件 函数
做什么 props 不变就跳过渲染 缓存计算结果 缓存函数引用
返回 新组件 缓存的值 缓存的函数

Q2:什么时候不该用 memo/useMemo?

  • 组件本身很轻量,渲染很快 → 加 memo 的对比开销反而更大
  • props 每次都会变 → memo 每次对比完还是要渲染,白对比了
  • 计算不复杂 → useMemo 的缓存开销 > 重新计算的开销

原则:先写正确的代码,遇到性能问题再优化,不要过早优化。

Q3:如何排查 React 性能问题?

  1. React DevTools Profiler --- 录制渲染,看哪些组件渲染了、耗时多久
  2. React.StrictMode --- 开发模式下会故意双重渲染,帮你发现副作用问题
  3. Chrome Performance --- 录制火焰图,看 JS 执行耗时
  4. why-did-you-render --- 第三方库,自动提示不必要的渲染

Q4:React 18 有哪些性能相关的新特性?

特性 说明
自动批处理 所有 setState 自动合并,减少渲染次数
useTransition 标记低优先级更新,不阻塞用户交互
useDeferredValue 延迟更新某个值,类似防抖
Suspense 改进 配合 lazy 和数据请求,更好的加载体验
jsx 复制代码
import { useTransition } from 'react';

function Search() {
  const [keyword, setKeyword] = useState('');
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();

  const handleChange = (e) => {
    setKeyword(e.target.value);  // 高优先级:立即更新输入框

    // 低优先级:搜索结果可以稍后更新,不阻塞输入
    startTransition(() => {
      setResults(filterData(e.target.value));
    });
  };

  return (
    <div>
      <input value={keyword} onChange={handleChange} />
      {isPending ? <p>搜索中...</p> : <ResultList data={results} />}
    </div>
  );
}
相关推荐
kyriewen1 小时前
盒模型:CSS 世界的物理法则,margin 塌陷与 padding 的恩怨情仇
前端·css·html
小成C1 小时前
别再把 Claude Code 用乱了:CLAUDE.md、Rules、Skills、Hooks 到底怎么分工?
前端·人工智能·面试
Kel1 小时前
这就是编程:Pi Monorepo 源码深度--解析一个工业级 AI Agent 框架的设计哲学
人工智能·设计模式·架构
巫山老妖1 小时前
OpenClaw 技术教程大全:从安装到多 Agent 协作,全在这里
java·前端
weixin_446260851 小时前
提升开发效率的神器!快速选择编码上下文 — React Grab
前端·react.js·前端框架
前端付豪1 小时前
自动学习建议解决薄弱知识点
前端·python·openai
SuperEugene2 小时前
Vite 实战教程:alias/env/proxy 配置 + 打包优化避坑|Vue 工程化篇
前端·javascript·vue.js·状态模式·vite
文心快码 Baidu Comate2 小时前
Comate 4.0的自我进化:后端“0帧起手”写前端、自己修自己!
前端·人工智能·后端·ai编程·文心快码·ai编程助手
geovindu2 小时前
python: Simple Factory Pattern
开发语言·python·设计模式·简单工厂模式