React 渲染优化:memo / useMemo / useCallback 的正确姿势与并发模式实战

0 阅读12分钟

前言

React 的声明式编程模型极大地提升了开发效率,但随着应用复杂度的增长,性能问题往往会悄然浮现:列表滚动卡顿、输入框延迟响应、页面切换不流畅。这些问题的根源大多指向同一个方向 -- 不必要的 re-render。

与此同时,React 18 引入的并发模式(Concurrent Mode)为性能优化提供了全新的思路。useTransition、useDeferredValue 等 API 让我们能够在不牺牲用户体验的前提下处理计算密集型任务。

本文将分为两大部分:第一部分系统梳理 React 渲染优化的基础手段(memo / useMemo / useCallback),包括正确用法和常见误区;第二部分深入并发模式的实战应用,帮助你在真实项目中做出合理的技术选择。


第一部分:React 渲染优化基础

一、React re-render 机制:组件何时会重新渲染?

理解优化之前,必须先理解 React 的渲染机制。一个组件会在以下情况下触发 re-render:

  1. 自身 state 变化 -- 调用 setState 后组件会重新渲染
  2. 父组件 re-render -- 父组件渲染时,所有子组件默认都会跟着渲染
  3. Context 值变化 -- 消费了某个 Context 的组件,当该 Context 的 value 变化时会重新渲染

注意:props 变化本身并不会触发 re-render,真正触发的是父组件的 re-render。即使 props 完全没变,子组件依然会重新执行。

// 父组件每次 re-render,ChildA 和 ChildB 都会重新渲染
// 即使 ChildB 的 props 没有任何变化
function Parent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <ChildA count={count} />
      <ChildB title="固定标题" /> {/* 依然会 re-render */}
      <button onClick={() => setCount(c => c + 1)}>+1</button>
    </div>
  );
}

这是 React 的默认行为,也是性能优化的核心出发点。

二、React.memo:避免不必要的子组件渲染

基本用法

React.memo 是一个高阶组件,它会对 props 进行浅比较,只有当 props 真正变化时才触发 re-render。

// 优化前:每次父组件渲染都会触发 ExpensiveList 重新渲染
function ExpensiveList({ items }: { items: string[] }) {
  console.log('ExpensiveList rendered');
  return (
    <ul>
      {items.map(item => (
        <li key={item}>{item}</li>
      ))}
    </ul>
  );
}

// 优化后:只有 items 引用变化时才会重新渲染
const MemoizedExpensiveList = React.memo(ExpensiveList);

function Parent() {
  const [count, setCount] = useState(0);
  const items = useMemo(() => ['React', 'Vue', 'Angular'], []);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
      <MemoizedExpensiveList items={items} />
    </div>
  );
}

自定义比较函数

对于复杂场景,可以传入第二个参数自定义比较逻辑:

const MemoizedChart = React.memo(Chart, (prevProps, nextProps) => {
  // 返回 true 表示 props 相同,跳过渲染
  // 返回 false 表示 props 不同,需要重新渲染
  return (
    prevProps.data.length === nextProps.data.length &&
    prevProps.data.every((item, i) => item.id === nextProps.data[i].id)
  );
});

何时使用 / 何时不使用

场景是否使用 memo原因
渲染开销大的组件(复杂列表、图表)使用避免昂贵的重复渲染
频繁 re-render 的父组件下的纯展示子组件使用子组件 props 稳定但被迫跟着渲染
组件本身非常轻量(一个 div 包文字)不使用memo 的比较开销可能比渲染本身还大
Props 几乎每次都会变化不使用浅比较白做,还额外增加开销
组件接收 children prop谨慎使用children 每次都是新的 JSX 对象,memo 形同虚设

三、useMemo:计算缓存与引用稳定性

useMemo 有两个主要用途:缓存昂贵的计算结果,以及保持对象/数组的引用稳定性。

用途一:缓存昂贵计算

function ProductList({ products, filter }: {
  products: Product[];
  filter: string;
}) {
  // 没有 useMemo:每次渲染都会执行过滤和排序
  // const filtered = products
  //   .filter(p => p.name.includes(filter))
  //   .sort((a, b) => a.price - b.price);

  // 使用 useMemo:只在 products 或 filter 变化时重新计算
  const filtered = useMemo(() => {
    console.log('重新计算过滤结果');
    return products
      .filter(p => p.name.includes(filter))
      .sort((a, b) => a.price - b.price);
  }, [products, filter]);

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

用途二:保持引用稳定性

这是 useMemo 更常见但容易被忽视的用途。当你把对象或数组作为 props 传给 memo 化的子组件时,必须保证引用稳定:

function Dashboard() {
  const [refreshKey, setRefreshKey] = useState(0);

  // 错误:每次渲染都会创建新的对象引用
  // const chartConfig = { theme: 'dark', animate: true };

  // 正确:引用稳定,不会导致子组件不必要的 re-render
  const chartConfig = useMemo(() => ({
    theme: 'dark',
    animate: true,
  }), []);

  // 同理,数组也需要 useMemo
  const columns = useMemo(() => [
    { key: 'name', title: '名称' },
    { key: 'price', title: '价格' },
    { key: 'stock', title: '库存' },
  ], []);

  return (
    <div>
      <button onClick={() => setRefreshKey(k => k + 1)}>刷新</button>
      <MemoizedChart config={chartConfig} />
      <MemoizedTable columns={columns} />
    </div>
  );
}

四、useCallback:函数引用稳定性

useCallback 本质上是 useMemo 的语法糖,专门用于缓存函数引用。

// useCallback(fn, deps) 等价于 useMemo(() => fn, deps)

典型用法

function TodoApp() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [filter, setFilter] = useState('');

  // 没有 useCallback:每次渲染创建新函数,MemoizedTodoItem 的 memo 失效
  // const handleToggle = (id: string) => {
  //   setTodos(prev => prev.map(t => t.id === id ? { ...t, done: !t.done } : t));
  // };

  // 使用 useCallback:函数引用稳定
  const handleToggle = useCallback((id: string) => {
    setTodos(prev => prev.map(t =>
      t.id === id ? { ...t, done: !t.done } : t
    ));
  }, []); // 使用函数式更新,无需依赖 todos

  const handleDelete = useCallback((id: string) => {
    setTodos(prev => prev.filter(t => t.id !== id));
  }, []);

  return (
    <div>
      <input value={filter} onChange={e => setFilter(e.target.value)} />
      {todos.map(todo => (
        <MemoizedTodoItem
          key={todo.id}
          todo={todo}
          onToggle={handleToggle}
          onDelete={handleDelete}
        />
      ))}
    </div>
  );
}

const MemoizedTodoItem = React.memo(function TodoItem({
  todo,
  onToggle,
  onDelete,
}: {
  todo: Todo;
  onToggle: (id: string) => void;
  onDelete: (id: string) => void;
}) {
  console.log(`TodoItem ${todo.id} rendered`);
  return (
    <div>
      <input
        type="checkbox"
        checked={todo.done}
        onChange={() => onToggle(todo.id)}
      />
      <span>{todo.text}</span>
      <button onClick={() => onDelete(todo.id)}>删除</button>
    </div>
  );
});

依赖陷阱

useCallback 最常见的问题是依赖项处理不当:

function SearchPanel({ onSearch }: { onSearch: (query: string) => void }) {
  const [query, setQuery] = useState('');
  const [category, setCategory] = useState('all');

  // 陷阱:category 在依赖中,每次切换分类都会生成新函数
  const handleSearch = useCallback(() => {
    onSearch(`${query} category:${category}`);
  }, [query, category, onSearch]);

  // 更好的方案:使用 ref 来避免频繁变化的依赖
  const stateRef = useRef({ query, category });
  stateRef.current = { query, category };

  const handleSearchStable = useCallback(() => {
    const { query, category } = stateRef.current;
    onSearch(`${query} category:${category}`);
  }, [onSearch]);

  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <select value={category} onChange={e => setCategory(e.target.value)}>
        <option value="all">全部</option>
        <option value="tech">技术</option>
      </select>
      <MemoizedSearchButton onClick={handleSearchStable} />
    </div>
  );
}

五、常见反模式

反模式一:到处使用 memo(过早优化)

// 错误:对轻量组件使用 memo 毫无意义
const MemoizedLabel = React.memo(({ text }: { text: string }) => (
  <span>{text}</span>
));

// 错误:对每个值都使用 useMemo
function Form() {
  const [name, setName] = useState('');
  // 没有必要,字符串拼接不是昂贵计算
  const greeting = useMemo(() => `你好, ${name}`, [name]);
  // 没有必要,没有传给 memo 组件
  const style = useMemo(() => ({ color: 'red' }), []);

  return <div style={style}>{greeting}</div>;
}

反模式二:useCallback 没有配合 React.memo

function Parent() {
  // 这个 useCallback 完全没用,因为 Child 没有用 React.memo 包裹
  const handleClick = useCallback(() => {
    console.log('clicked');
  }, []);

  // Child 每次都会 re-render,useCallback 只是白白增加了内存开销
  return <Child onClick={handleClick} />;
}

反模式三:依赖项遗漏

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

  // 错误:遗漏了 count 依赖,闭包中的 count 永远是 0
  const increment = useCallback(() => {
    setCount(count + 1); // 永远是 0 + 1 = 1
  }, []); // eslint 会发出警告

  // 正确:使用函数式更新来避免依赖
  const incrementCorrect = useCallback(() => {
    setCount(prev => prev + 1);
  }, []);

  return <button onClick={incrementCorrect}>{count}</button>;
}

六、React DevTools Profiler 定位不必要的渲染

理论知识需要结合工具实践。React DevTools 的 Profiler 是定位渲染性能问题的利器。

使用步骤:

  1. 安装 React DevTools 浏览器扩展
  2. 打开 DevTools,切换到 Profiler 面板
  3. 点击录制按钮,执行需要分析的操作
  4. 停止录制,查看火焰图(Flamegraph)

关键指标解读:

- 灰色组件:本次未渲染(被 memo 跳过)
- 绿色/黄色/红色组件:本次有渲染,颜色越深耗时越长
- "Why did this render?" 面板:显示组件重新渲染的原因
  - Props changed: (onClick)  -> 说明 onClick 引用变了
  - The parent component rendered -> 说明是被父组件带动的

在设置中开启 "Highlight updates when components render" 可以在页面上直观地看到哪些组件在闪烁。如果你输入一个字符,整个页面都在闪烁,那就说明需要优化了。

七、State Colocation:把状态放到离使用它最近的地方

在讨论 memo 之前,有一个更简单且更有效的优化策略往往被忽视 -- State Colocation(状态就近放置)。

// 优化前:搜索状态放在顶层,每次输入都导致整个页面 re-render
function Page() {
  const [searchQuery, setSearchQuery] = useState('');
  const [data, setData] = useState<Item[]>([]);

  return (
    <div>
      <input
        value={searchQuery}
        onChange={e => setSearchQuery(e.target.value)}
      />
      <ExpensiveHeader />
      <ExpensiveSidebar />
      <DataGrid data={data} />
    </div>
  );
}

// 优化后:将搜索状态下沉到独立的搜索组件中
function Page() {
  const [data, setData] = useState<Item[]>([]);

  return (
    <div>
      <SearchBar onSearch={(q) => fetchData(q).then(setData)} />
      <ExpensiveHeader />
      <ExpensiveSidebar />
      <DataGrid data={data} />
    </div>
  );
}

function SearchBar({ onSearch }: { onSearch: (q: string) => void }) {
  const [query, setQuery] = useState('');

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setQuery(e.target.value);
    onSearch(e.target.value);
  };

  return <input value={query} onChange={handleChange} />;
}

这样做的好处是:输入框的每次 keystroke 只会触发 SearchBar 的 re-render,而不会波及 ExpensiveHeader、ExpensiveSidebar 等无关组件。

八、状态管理层的优化

全局状态管理是 re-render 问题的重灾区。如果不注意,一个状态变化可能导致几十个组件同时 re-render。

Zustand:使用 selector 精确订阅

import { create } from 'zustand';

interface AppStore {
  user: { name: string; avatar: string };
  theme: 'light' | 'dark';
  notifications: Notification[];
  setTheme: (theme: 'light' | 'dark') => void;
  addNotification: (n: Notification) => void;
}

const useAppStore = create<AppStore>((set) => ({
  user: { name: '张三', avatar: '/avatar.png' },
  theme: 'light',
  notifications: [],
  setTheme: (theme) => set({ theme }),
  addNotification: (n) => set((s) => ({
    notifications: [...s.notifications, n],
  })),
}));

// 错误:订阅整个 store,任何状态变化都会触发 re-render
function Header() {
  const store = useAppStore(); // 任何字段变化都会渲染
  return <div>{store.user.name}</div>;
}

// 正确:使用 selector 精确订阅需要的字段
function Header() {
  const userName = useAppStore((s) => s.user.name);
  return <div>{userName}</div>;
}

// 正确:订阅多个字段时使用 shallow 比较
import { shallow } from 'zustand/shallow';

function UserPanel() {
  const { name, avatar } = useAppStore(
    (s) => ({ name: s.user.name, avatar: s.user.avatar }),
    shallow,
  );
  return (
    <div>
      <img src={avatar} alt={name} />
      <span>{name}</span>
    </div>
  );
}

Jotai:原子化状态天然避免多余渲染

import { atom, useAtom, useAtomValue } from 'jotai';

// 每个 atom 是独立的订阅单元
const userAtom = atom({ name: '张三', avatar: '/avatar.png' });
const themeAtom = atom<'light' | 'dark'>('light');
const notificationsAtom = atom<Notification[]>([]);

// 派生 atom:只在依赖变化时重新计算
const unreadCountAtom = atom((get) => {
  const notifications = get(notificationsAtom);
  return notifications.filter(n => !n.read).length;
});

function NotificationBadge() {
  // 只有未读数量变化时才会 re-render
  const unreadCount = useAtomValue(unreadCountAtom);
  return unreadCount > 0 ? <span>{unreadCount}</span> : null;
}

function ThemeToggle() {
  const [theme, setTheme] = useAtom(themeAtom);
  // 用户信息和通知变化不会影响这个组件
  return (
    <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
      当前主题:{theme}
    </button>
  );
}

第二部分:React 并发模式实战

一、什么是并发渲染

在传统的同步渲染模式下,React 一旦开始渲染就无法中断,直到整个组件树渲染完成。如果渲染过程耗时较长(比如一个包含上千个节点的列表),页面就会出现卡顿,用户的输入得不到及时响应。

并发渲染(Concurrent Rendering)是 React 18 引入的底层机制,它让 React 具备了以下能力:

  • 可中断渲染:React 可以暂停正在进行的渲染工作,优先处理更紧急的更新
  • 优先级调度:用户输入(高优先级)可以打断数据加载后的列表渲染(低优先级)
  • 增量渲染:将大的渲染任务拆分成小块,穿插在浏览器的空闲时间执行

并发渲染并不是一个需要手动开启的"模式",而是 React 18 的底层架构升级。开发者通过 useTransition 和 useDeferredValue 等 API 来告诉 React 哪些更新是可以被延迟的。

二、useTransition:标记非紧急更新

useTransition 允许你将某些 state 更新标记为"过渡更新"(transition),这些更新的优先级较低,不会阻塞用户输入等高优先级操作。

基础用法

function TabContainer() {
  const [activeTab, setActiveTab] = useState('home');
  const [isPending, startTransition] = useTransition();

  const handleTabChange = (tab: string) => {
    // 高优先级:立即更新 tab 的激活状态(视觉反馈)
    // 但 tab 内容的渲染可以是低优先级的
    startTransition(() => {
      setActiveTab(tab);
    });
  };

  return (
    <div>
      <nav>
        {['home', 'analytics', 'settings'].map(tab => (
          <button
            key={tab}
            onClick={() => handleTabChange(tab)}
            className={activeTab === tab ? 'active' : ''}
          >
            {tab}
          </button>
        ))}
      </nav>
      {isPending && <div className="loading-bar" />}
      <TabContent tab={activeTab} />
    </div>
  );
}

实战:搜索过滤场景

这是 useTransition 最经典的应用场景。输入框需要即时响应,但过滤大列表的渲染可以延迟:

interface Product {
  id: string;
  name: string;
  category: string;
  price: number;
}

function ProductSearch({ products }: { products: Product[] }) {
  const [query, setQuery] = useState('');
  const [filteredQuery, setFilteredQuery] = useState('');
  const [isPending, startTransition] = useTransition();

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = e.target.value;
    // 高优先级:立即更新输入框
    setQuery(value);
    // 低优先级:延迟更新过滤结果
    startTransition(() => {
      setFilteredQuery(value);
    });
  };

  const filteredProducts = useMemo(() => {
    if (!filteredQuery) return products;
    const lowerQuery = filteredQuery.toLowerCase();
    return products.filter(p =>
      p.name.toLowerCase().includes(lowerQuery) ||
      p.category.toLowerCase().includes(lowerQuery)
    );
  }, [products, filteredQuery]);

  return (
    <div>
      <input
        value={query}
        onChange={handleChange}
        placeholder="搜索商品..."
      />
      {isPending && <p style={{ color: '#999' }}>正在搜索...</p>}
      <div style={{ opacity: isPending ? 0.7 : 1, transition: 'opacity 0.2s' }}>
        <p>共 {filteredProducts.length} 条结果</p>
        {filteredProducts.map(product => (
          <ProductCard key={product.id} product={product} />
        ))}
      </div>
    </div>
  );
}

const ProductCard = React.memo(function ProductCard({
  product,
}: {
  product: Product;
}) {
  // 模拟一个渲染开销较大的商品卡片
  return (
    <div className="product-card">
      <h3>{product.name}</h3>
      <span>{product.category}</span>
      <span>{product.price}元</span>
    </div>
  );
});

三、useDeferredValue:延迟昂贵的重渲染

useDeferredValue 是另一种并发优化手段,它接收一个值并返回该值的"延迟版本"。当原始值频繁变化时,延迟版本会滞后更新,让 React 优先完成更重要的渲染。

与 useTransition 的区别

  • useTransition 作用于 setState 调用,你主动控制哪些更新是低优先级的
  • useDeferredValue 作用于 值本身,适用于你无法控制值的产生方式的场景(比如值来自 props)

实战:列表过滤

function FilterableList({ items }: { items: string[] }) {
  const [filter, setFilter] = useState('');
  // filter 立即更新(输入框响应快),deferredFilter 延迟更新(列表渲染可以慢一点)
  const deferredFilter = useDeferredValue(filter);
  const isStale = filter !== deferredFilter;

  const filteredItems = useMemo(() => {
    return items.filter(item =>
      item.toLowerCase().includes(deferredFilter.toLowerCase())
    );
  }, [items, deferredFilter]);

  return (
    <div>
      <input
        value={filter}
        onChange={e => setFilter(e.target.value)}
        placeholder="输入关键词过滤..."
      />
      <div style={{ opacity: isStale ? 0.6 : 1, transition: 'opacity 0.15s' }}>
        <ul>
          {filteredItems.map((item, i) => (
            <SlowItem key={i} text={item} />
          ))}
        </ul>
      </div>
    </div>
  );
}

// 模拟渲染较慢的列表项
const SlowItem = React.memo(function SlowItem({ text }: { text: string }) {
  const startTime = performance.now();
  while (performance.now() - startTime < 1) {
    // 模拟每个 item 渲染耗时 1ms
    // 如果有 1000 个 item,总渲染时间就是 1 秒
  }
  return <li>{text}</li>;
});

四、Suspense 与数据加载

Suspense 在并发模式下的能力得到了极大扩展,特别是在服务端渲染场景中:

Streaming SSR 与渐进式加载

// app/page.tsx (Next.js App Router)
import { Suspense } from 'react';

export default function DashboardPage() {
  return (
    <div className="dashboard">
      {/* 页面框架立即渲染 */}
      <header>
        <h1>数据看板</h1>
        <nav>...</nav>
      </header>

      <div className="grid">
        {/* 每个区域独立加载,先到先显示 */}
        <Suspense fallback={<ChartSkeleton />}>
          <RevenueChart />
        </Suspense>

        <Suspense fallback={<TableSkeleton />}>
          <OrderTable />
        </Suspense>

        <Suspense fallback={<StatsSkeleton />}>
          <UserStats />
        </Suspense>
      </div>
    </div>
  );
}

// 每个组件内部独立获取数据
async function RevenueChart() {
  const data = await fetchRevenueData(); // 服务端数据获取
  return <Chart data={data} type="line" />;
}

async function OrderTable() {
  const orders = await fetchOrders();
  return <DataTable rows={orders} />;
}

async function UserStats() {
  const stats = await fetchUserStats();
  return (
    <div className="stats-grid">
      <StatCard title="日活用户" value={stats.dau} />
      <StatCard title="新增用户" value={stats.newUsers} />
      <StatCard title="转化率" value={`${stats.conversion}%`} />
    </div>
  );
}

在 Streaming SSR 模式下,服务端会先发送 HTML 骨架和 fallback 内容,然后各个 Suspense 边界内的内容加载完成后逐步流式传输到客户端。用户不需要等待所有数据加载完毕就能看到并与页面交互。

五、对比:useTransition vs useDeferredValue vs debounce

三者都能解决"频繁更新导致卡顿"的问题,但机制和适用场景不同:

维度useTransitionuseDeferredValuedebounce
作用对象setState 调用回调函数
是否需要控制状态更新源
适用于 props 来源的值不适合适合不适合
渲染是否可中断否(只是延迟触发)
是否丢弃中间状态的渲染是(从源头减少触发)
是否提供 pending 状态是(isPending)需要手动比较需要手动管理
首次更新的延迟有(等待 debounce 时间)
是否依赖 React 并发特性

选择建议:

你能控制 setState 的调用吗?
├── 能 → 需要 isPending 状态吗?
│   ├── 需要 → useTransition
│   └── 不需要 → useTransition 或 debounce 皆可
└── 不能(值来自 props / 外部) → useDeferredValue

需要兼容 React 17 或更早版本? → debounce
需要减少 API 请求次数? → debounce(并发 API 不减少请求,只优化渲染)

六、综合实战:可交互的数据看板

下面用一个完整的例子将本文的所有知识点串联起来:一个包含重型图表的数据看板,在筛选条件变化时保持流畅的交互体验。

import React, {
  useState,
  useMemo,
  useCallback,
  useTransition,
  useDeferredValue,
  Suspense,
} from 'react';
import { create } from 'zustand';

// ---------- 状态管理(Zustand + selector)----------

interface DashboardFilters {
  dateRange: [string, string];
  region: string;
  category: string;
}

interface DashboardStore {
  filters: DashboardFilters;
  setFilters: (filters: Partial<DashboardFilters>) => void;
}

const useDashboardStore = create<DashboardStore>((set) => ({
  filters: {
    dateRange: ['2025-01-01', '2025-12-31'],
    region: 'all',
    category: 'all',
  },
  setFilters: (partial) =>
    set((state) => ({
      filters: { ...state.filters, ...partial },
    })),
}));

// ---------- 筛选栏组件 ----------

function FilterBar() {
  const filters = useDashboardStore((s) => s.filters);
  const setFilters = useDashboardStore((s) => s.setFilters);
  const [isPending, startTransition] = useTransition();

  const handleRegionChange = useCallback(
    (e: React.ChangeEvent<HTMLSelectElement>) => {
      // 使用 transition 标记筛选变更为非紧急更新
      startTransition(() => {
        setFilters({ region: e.target.value });
      });
    },
    [setFilters],
  );

  const handleCategoryChange = useCallback(
    (e: React.ChangeEvent<HTMLSelectElement>) => {
      startTransition(() => {
        setFilters({ category: e.target.value });
      });
    },
    [setFilters],
  );

  return (
    <div className="filter-bar">
      <select value={filters.region} onChange={handleRegionChange}>
        <option value="all">全部地区</option>
        <option value="north">华北</option>
        <option value="south">华南</option>
        <option value="east">华东</option>
      </select>

      <select value={filters.category} onChange={handleCategoryChange}>
        <option value="all">全部品类</option>
        <option value="electronics">电子产品</option>
        <option value="clothing">服装</option>
        <option value="food">食品</option>
      </select>

      {isPending && <span className="filter-loading">图表更新中...</span>}
    </div>
  );
}

// ---------- 搜索组件(useDeferredValue)----------

function OrderSearch({ orders }: { orders: Order[] }) {
  const [searchText, setSearchText] = useState('');
  const deferredSearch = useDeferredValue(searchText);
  const isStale = searchText !== deferredSearch;

  const filteredOrders = useMemo(() => {
    if (!deferredSearch) return orders.slice(0, 100);
    const lower = deferredSearch.toLowerCase();
    return orders.filter(
      (o) =>
        o.customerName.toLowerCase().includes(lower) ||
        o.orderId.toLowerCase().includes(lower),
    );
  }, [orders, deferredSearch]);

  return (
    <div>
      <input
        value={searchText}
        onChange={(e) => setSearchText(e.target.value)}
        placeholder="搜索订单号或客户名..."
      />
      <div style={{ opacity: isStale ? 0.6 : 1 }}>
        <MemoizedOrderTable orders={filteredOrders} />
      </div>
    </div>
  );
}

// ---------- 重型图表组件(React.memo + useMemo)----------

interface ChartData {
  labels: string[];
  values: number[];
}

const HeavyChart = React.memo(function HeavyChart({
  data,
  title,
}: {
  data: ChartData;
  title: string;
}) {
  // 模拟图表库的复杂渲染
  console.log(`[HeavyChart] ${title} rendered`);

  const processedData = useMemo(() => {
    // 模拟数据处理:计算移动平均、趋势线等
    const movingAvg = data.values.map((_, i, arr) => {
      const start = Math.max(0, i - 2);
      const window = arr.slice(start, i + 1);
      return window.reduce((a, b) => a + b, 0) / window.length;
    });
    return { ...data, movingAvg };
  }, [data]);

  return (
    <div className="chart-container">
      <h3>{title}</h3>
      <div className="chart-canvas">
        {/* 实际项目中这里是 ECharts / Recharts 等图表库 */}
        {processedData.labels.map((label, i) => (
          <div key={label} className="bar" style={{
            height: `${processedData.values[i]}%`,
          }}>
            <span>{label}: {processedData.values[i]}</span>
          </div>
        ))}
      </div>
    </div>
  );
});

// ---------- 订单表格(React.memo)----------

interface Order {
  orderId: string;
  customerName: string;
  amount: number;
  status: string;
}

const MemoizedOrderTable = React.memo(function OrderTable({
  orders,
}: {
  orders: Order[];
}) {
  console.log(`[OrderTable] rendered with ${orders.length} orders`);
  return (
    <table>
      <thead>
        <tr>
          <th>订单号</th>
          <th>客户</th>
          <th>金额</th>
          <th>状态</th>
        </tr>
      </thead>
      <tbody>
        {orders.map((order) => (
          <tr key={order.orderId}>
            <td>{order.orderId}</td>
            <td>{order.customerName}</td>
            <td>{order.amount.toFixed(2)}</td>
            <td>{order.status}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
});

// ---------- 看板主页面 ----------

function Dashboard() {
  const filters = useDashboardStore((s) => s.filters);

  // 根据筛选条件生成图表数据,useMemo 缓存避免重复计算
  const revenueData = useMemo<ChartData>(() => {
    return computeRevenueData(filters);
  }, [filters]);

  const orderTrendData = useMemo<ChartData>(() => {
    return computeOrderTrend(filters);
  }, [filters]);

  const categoryData = useMemo<ChartData>(() => {
    return computeCategoryDistribution(filters);
  }, [filters]);

  return (
    <div className="dashboard">
      <h1>运营数据看板</h1>
      <FilterBar />

      <div className="chart-grid">
        <Suspense fallback={<div className="skeleton chart-skeleton" />}>
          <HeavyChart data={revenueData} title="营收趋势" />
        </Suspense>

        <Suspense fallback={<div className="skeleton chart-skeleton" />}>
          <HeavyChart data={orderTrendData} title="订单趋势" />
        </Suspense>

        <Suspense fallback={<div className="skeleton chart-skeleton" />}>
          <HeavyChart data={categoryData} title="品类分布" />
        </Suspense>
      </div>

      <Suspense fallback={<div className="skeleton table-skeleton" />}>
        <OrderSearch orders={mockOrders} />
      </Suspense>
    </div>
  );
}

这个看板综合运用了以下优化策略:

  1. Zustand selector -- FilterBar 和 Dashboard 各自精确订阅需要的 store 字段,互不影响
  2. useTransition -- 筛选条件变化时标记为非紧急更新,下拉框的交互不会被图表渲染阻塞
  3. useDeferredValue -- 订单搜索框的输入即时响应,表格渲染延迟跟进
  4. React.memo -- HeavyChart 和 OrderTable 都做了 memo 处理,避免无关状态变化导致的重复渲染
  5. useMemo -- 图表数据和搜索过滤结果都做了缓存,避免重复计算
  6. Suspense -- 各区域独立加载,不会互相阻塞

总结

React 性能优化并不是把所有组件都用 memo 包一层那么简单。合理的优化需要建立在对渲染机制的深入理解之上。

渲染优化的决策顺序应该是:

  1. 先做 State Colocation -- 把状态放到离使用它最近的地方,这是零成本的优化
  2. 再考虑状态管理设计 -- 使用 Zustand selector、Jotai atom 等方式精确订阅
  3. 然后才是 memo / useMemo / useCallback -- 针对 Profiler 中确认的性能瓶颈进行优化
  4. 最后考虑并发特性 -- useTransition 和 useDeferredValue 适合处理确实无法避免的昂贵渲染

记住:先度量,再优化。React DevTools Profiler 是你最好的朋友。不要凭直觉优化,不要过早优化,让数据告诉你瓶颈在哪里。

如果觉得有帮助,欢迎点赞收藏关注,后续会继续分享前端性能优化相关的实践。