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 查看我的更多作品。欢迎大家来观看!

相关推荐
南屹川8 小时前
【网络】TCP/IP协议深度解析:从连接建立到数据传输
人工智能
糯米团子7498 小时前
vue知识点复习
前端·vue.js
月诸清酒8 小时前
66-260522 AI 科技日报 (谷歌永久提高Antigravity平台的Gemini使用限额到3倍)
人工智能
龙腾AI白云8 小时前
【无标题】知识图谱:AI的超级大脑
人工智能·知识图谱·tornado
土星云SaturnCloud8 小时前
土星云边缘计算设备的多模态模型部署实操
服务器·人工智能·ai·边缘计算
范同学~8 小时前
多个表单如何用element ui 校验
javascript·vue.js·ui
晚烛8 小时前
CANN 日志系统:调试与性能分析的日志艺术
前端·chrome·数据挖掘
晓杰'8 小时前
Balatro后端进阶(2):基于GitHub Actions的CI自动化验证实现
websocket·ci/cd·typescript·node.js·自动化·github·nestjs
FlyWIHTSKY8 小时前
Next中引入 Ant Design (antd)的配置
开发语言·前端·javascript