前言
React 的声明式编程模型极大地提升了开发效率,但随着应用复杂度的增长,性能问题往往会悄然浮现:列表滚动卡顿、输入框延迟响应、页面切换不流畅。这些问题的根源大多指向同一个方向 -- 不必要的 re-render。
与此同时,React 18 引入的并发模式(Concurrent Mode)为性能优化提供了全新的思路。useTransition、useDeferredValue 等 API 让我们能够在不牺牲用户体验的前提下处理计算密集型任务。
本文将分为两大部分:第一部分系统梳理 React 渲染优化的基础手段(memo / useMemo / useCallback),包括正确用法和常见误区;第二部分深入并发模式的实战应用,帮助你在真实项目中做出合理的技术选择。
第一部分:React 渲染优化基础
一、React re-render 机制:组件何时会重新渲染?
理解优化之前,必须先理解 React 的渲染机制。一个组件会在以下情况下触发 re-render:
- 自身 state 变化 -- 调用 setState 后组件会重新渲染
- 父组件 re-render -- 父组件渲染时,所有子组件默认都会跟着渲染
- 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 是定位渲染性能问题的利器。
使用步骤:
- 安装 React DevTools 浏览器扩展
- 打开 DevTools,切换到 Profiler 面板
- 点击录制按钮,执行需要分析的操作
- 停止录制,查看火焰图(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>
);
}
这个看板综合运用了以下优化策略:
- Zustand selector -- FilterBar 和 Dashboard 各自精确订阅需要的 store 字段,互不影响
- useTransition -- 筛选条件变化时标记为非紧急更新,下拉框的交互不会被图表渲染阻塞
- useDeferredValue -- 订单搜索框的输入即时响应,表格渲染延迟跟进
- React.memo -- HeavyChart 和 OrderTable 都做了 memo 处理,避免无关状态变化导致的重复渲染
- useMemo -- 图表数据和搜索过滤结果都做了缓存,避免重复计算
- Suspense -- 各区域独立加载,不会互相阻塞
总结
React 性能优化并不是把所有组件都用 memo 包一层那么简单。合理的优化需要建立在对渲染机制的深入理解之上。
渲染优化的决策顺序应该是:
- 先做 State Colocation -- 把状态放到离使用它最近的地方,这是零成本的优化
- 再考虑状态管理设计 -- 使用 Zustand selector、Jotai atom 等方式精确订阅
- 然后才是 memo / useMemo / useCallback -- 针对 Profiler 中确认的性能瓶颈进行优化
- 最后考虑并发特性 -- useTransition 和 useDeferredValue 适合处理确实无法避免的昂贵渲染
记住:先度量,再优化。React DevTools Profiler 是你最好的朋友。不要凭直觉优化,不要过早优化,让数据告诉你瓶颈在哪里。
如果觉得有帮助,欢迎点赞收藏关注,后续会继续分享前端性能优化相关的实践。