React 从入门到生产(七):性能优化实战

创作者: Yardon | GitHub: github.com/YardonYan | 版本: v1.0



性能优化的思维方式

性能优化有一条铁律:先测量,再优化。

很多开发者凭直觉去优化代码,结果往往是:花了大量时间把一些「看起来慢」的地方优化好,但真正的瓶颈根本没碰。

React 的性能问题通常出在三个地方:

  1. 不必要的重新渲染------组件被重复渲染,但 props/state 根本没变
  2. 昂贵的计算------每次渲染都在重复执行开销很大的计算
  3. 资源体积过大------JS 包太大、图片未优化、字体未压缩

优化之前,问问自己:这真的是瓶颈吗?开发模式下的 React 比生产模式慢 10 倍------你看到的卡顿可能只是开发模式的特有现象。


React.memo:阻止不必要的渲染

React 的默认行为是:父组件重新渲染时,所有子组件也重新渲染。这通常不是问题,但当组件树很大时,一次小的状态更新可能导致几十上百个组件重新渲染。

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

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>{count}</button>
      {/* HeavyComponent 的 props 没变,但每次 count 变它都重新渲染 */}
      <HeavyComponent name={name} />
    </div>
  );
}

React.memo 就像一个过滤器:如果 props 没变,就跳过渲染。

jsx 复制代码
const HeavyComponent = React.memo(function HeavyComponent({ name }) {
  // 昂贵的渲染逻辑...
  console.log('渲染了');
  return <div>{name}</div>;
});

现在 count 无论如何变化,HeavyComponent 都不会重新渲染------因为它的 name prop 没变。

自定义比较函数

默认情况下 React.memo 做浅比较(===)。对于复杂对象,可以传自定义比较函数:

jsx 复制代码
const areEqual = (prevProps, nextProps) => {
  return prevProps.user.id === nextProps.user.id &&
         prevProps.user.name === nextProps.user.name;
};

const UserCard = React.memo(function UserCard({ user }) {
  // 只有 user 的关键字段变化才渲染
}, areEqual);

useMemo 和 useCallback

这两个 Hook 经常被滥用。在 80% 的情况下,你不需要它们。

useMemo:记忆计算结果

jsx 复制代码
function Dashboard({ transactions }) {
  // ❌ 每次渲染都重新计算
  const totals = {
    income: transactions.filter(t => t.type === 'income')
      .reduce((sum, t) => sum + t.amount, 0),
    expense: transactions.filter(t => t.type === 'expense')
      .reduce((sum, t) => sum + t.amount, 0),
  };

  return <Charts totals={totals} />;
}

function Dashboard({ transactions }) {
  // ✅ 只在 transactions 变化时重新计算
  const totals = useMemo(() => ({
    income: transactions.filter(t => t.type === 'income')
      .reduce((sum, t) => sum + t.amount, 0),
    expense: transactions.filter(t => t.type === 'expense')
      .reduce((sum, t) => sum + t.amount, 0),
  }), [transactions]);

  return <Charts totals={totals} />;
}

useCallback:保持函数引用的稳定性

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

  // ❌ 每次渲染创建一个新函数
  const handleClick = () => {
    doSomething(count);
  };

  // ✅ 只在 count 变化时创建新函数
  const handleClick = useCallback(() => {
    doSomething(count);
  }, [count]);

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

只有当你把函数传给 React.memo 包装的子组件,或者把函数放进 useEffect 的依赖数组时,useCallback 才有意义。单独使用它不会带来性能提升------它本身也有开销。


列表优化:key 与虚拟滚动

Key 的正确使用

jsx 复制代码
// ❌ 用 index 做 key(列表元素重排时出问题)
{items.map((item, index) => <li key={index}>{item.name}</li>)}

// ✅ 用唯一标识做 key
{items.map(item => <li key={item.id}>{item.name}</li>)}

虚拟滚动

当列表有成千上万条数据时,一次性渲染所有 DOM 节点会让浏览器崩溃。虚拟滚动只渲染可视区域内的元素。

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

function VirtualList({ items }) {
  return (
    <FixedSizeList
      height={600}
      itemCount={items.length}
      itemSize={50}
      width="100%"
    >
      {({ index, style }) => (
        <div style={style}>{items[index].name}</div>
      )}
    </FixedSizeList>
  );
}

react-window 只有约 3KB,但能把 10000 条数据的渲染时间从数秒降到毫秒级。


代码分割与 Tree Shaking

动态 import

jsx 复制代码
// 路由级分割
const Dashboard = lazy(() => import('./pages/Dashboard'));

// 组件级分割
const HeavyChart = lazy(() => import('./components/HeavyChart'));

Tree Shaking

Vite/Webpack 的 Tree Shaking 依赖于 ESM 的静态分析------它能在打包时移除未使用的代码。

jsx 复制代码
// ❌ lodash 的 CJS 方式不支持 tree-shaking
const _ = require('lodash');
_.sortBy(data, 'name');

// ✅ 按需导入
import sortBy from 'lodash/sortBy';
sortBy(data, 'name');

图片与资源优化

jsx 复制代码
// srcset 根据屏幕宽度加载不同尺寸的图片
<img
  src="/hero-1200w.webp"
  srcSet="/hero-400w.webp 400w, /hero-800w.webp 800w, /hero-1200w.webp 1200w"
  sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px"
  loading="lazy"  // 懒加载
  alt="描述"
/>

Profiler:找到真正的瓶颈

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

function onRenderCallback(id, phase, actualDuration) {
  console.log(`${id} ${phase}: ${actualDuration}ms`);
}

function App() {
  return (
    <Profiler id="Navigation" onRender={onRenderCallback}>
      <Navbar />
    </Profiler>
  );
}
指标 含义
actualDuration 组件本次渲染的实际耗时
phase mount(首次)或 update(更新)

本章小结

技术 使用场景
React.memo props 不变的组件免渲染
useMemo 昂贵的计算、防止子组件不必要的渲染
useCallback 传递给 memo 组件的函数保持引用稳定
lazy + Suspense 按需加载页面,减小首屏体积
react-window 长列表虚拟滚动
Profiler 定位真正的性能瓶颈

下一章是本系列的最后一章------我们聊聊测试与部署,让你的应用从开发环境走向线上。


📌 创作者: Yardon | 🏠 个人网站: GlimmerAI.top

📖 本章是「React 从入门到生产 」系列的第 7 章。下一章:测试与部署

🌟 如果你觉得有帮助,欢迎访问 GlimmerAI.top 查看我的更多作品。欢迎大家来观看!

相关推荐
xyphf_和派孔明12 分钟前
企业级微前端项目完整创建步骤,第三步:配置 Vue 子应用
前端·javascript
自强的小白13 分钟前
条件查询或查询全部时时前端请求数据等时间格式
前端
巫山老妖15 分钟前
AI驱动的Flutter工程:如何让AI生成代码不跑偏
人工智能
深度学习lover24 分钟前
<数据集>yolo 小麦麦穗识别<目标检测>
人工智能·yolo·目标检测·计算机视觉·数据集·小麦麦穗识别
火山引擎开发者社区25 分钟前
让每台终端都成为智能入口:ArkClaw × 云沙箱的规模化实战
人工智能
阳光是sunny31 分钟前
LangGraph实战教程:一文搞懂图的状态(State)管理
前端·人工智能·后端
数智化管理手记36 分钟前
财务大数据怎么管住资金风险?财务大数据和财务数智化到底怎么结合?
大数据·网络·数据库·人工智能·数据挖掘
whyfail44 分钟前
前端学 Spring Boot(5):从“你是谁”到“服务真的上线了”
前端·spring boot·后端
AI导出鸭1 小时前
如何让deepseek生成word文档 ?「AI 导出鸭」苹果版:从API流式解析到Pages级渲染,硬核攻克公式裂变与表格回流的终极方案。
人工智能·chatgpt·word·cocoa·ai导出鸭
涛声依旧god1 小时前
如何打造一个 AI Agent 自动写作并一键发布技术文章的自动化系统
人工智能·ai·自动化·ai编程