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

前言

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 完全没变,子组件依然会重新执行。

tsx 复制代码
// 父组件每次 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。

tsx 复制代码
// 优化前:每次父组件渲染都会触发 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>
  );
}

自定义比较函数

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

tsx 复制代码
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 有两个主要用途:缓存昂贵的计算结果,以及保持对象/数组的引用稳定性。

用途一:缓存昂贵计算

tsx 复制代码
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 化的子组件时,必须保证引用稳定:

tsx 复制代码
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 的语法糖,专门用于缓存函数引用。

tsx 复制代码
// useCallback(fn, deps) 等价于 useMemo(() => fn, deps)

典型用法

tsx 复制代码
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 最常见的问题是依赖项处理不当:

tsx 复制代码
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(过早优化)

tsx 复制代码
// 错误:对轻量组件使用 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

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

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

反模式三:依赖项遗漏

tsx 复制代码
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)

关键指标解读:

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

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

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

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

tsx 复制代码
// 优化前:搜索状态放在顶层,每次输入都导致整个页面 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 精确订阅

tsx 复制代码
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:原子化状态天然避免多余渲染

tsx 复制代码
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),这些更新的优先级较低,不会阻塞用户输入等高优先级操作。

基础用法

tsx 复制代码
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 最经典的应用场景。输入框需要即时响应,但过滤大列表的渲染可以延迟:

tsx 复制代码
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)

实战:列表过滤

tsx 复制代码
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 与渐进式加载

tsx 复制代码
// 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

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

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

选择建议:

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

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

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

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

tsx 复制代码
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 是你最好的朋友。不要凭直觉优化,不要过早优化,让数据告诉你瓶颈在哪里。

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

相关推荐
杨利杰YJlio1 小时前
Codex桌面客户端上手:项目、插件与自动化实战
前端·后端
胡志辉1 小时前
从 prototype 到 V8,看懂 JavaScript 原型链
前端·javascript
ClouGence1 小时前
零代码自动化测试:手把手教你录出一条能反复用的测试用例
前端·测试
常铭1 小时前
【Java基础】01-HashMap的底层原理
后端·面试
skiyee1 小时前
🔥UniApp 仅需 5 行代码!实现所有页面中控制应用主题变化
前端·微信小程序
LaiYoung_1 小时前
🎁 送你一套超好用超实用的 FE AI-Coding Skills
前端·人工智能·开源
幼儿园技术家2 小时前
实现 GEO 监控:从多引擎探测到优化闭环
前端·后端
甲维斯2 小时前
GLM5.2+ZCode复刻坦克大战,自测50万帧!
前端·ai编程·游戏开发
Csvn2 小时前
useRef 的 5 个冷门但救命的高级用法
前端