
当你的React应用越来越慢,但Profile工具找不到明显瓶颈时,问题可能不在代码细节,而在状态管理架构本身。
去年做一个订单数据看板时,遇到了一个很典型的问题。
功能很简单:
但上线后用户抱怨:"每次我改个筛选条件,页面就卡好几秒,体验太差了。"
我用Chrome的性能工具看了半天:
但就是不快。后来我琢磨了很久才明白:不是React慢,是我用React的方式有问题。
就像你开车很慢,问题可能不是车不好,而是你一直在用一档开高速。
让我用一个生活场景来解释这个问题:
想象你在一个公司食堂订餐:
这就是React状态管理的"牵一发动全身"问题。
再看看代码层面,当时我是这么写的:
// 我把所有数据都放在一个Context里
const DashboardContext = createContext();
function DashboardProvider({ children }) {
// 用户信息
const [userInfo, setUserInfo] = useState(null);
// 订单数据
const [orders, setOrders] = useState([]);
// 筛选条件
const [filters, setFilters] = useState({
dateRange: 'last7days',
channel: 'all'
});
// 加载状态
const [loading, setLoading] = useState(false);
// 关键问题:每次filters变化,就重新请求数据
useEffect(() => {
setLoading(true);
fetchOrders(filters).then(data => {
setOrders(data);
setLoading(false);
});
}, [filters]); // filters一变,这里就执行
return (
<DashboardContext.Provider value={{
userInfo, orders, filters, loading,
setFilters, setUserInfo
}}>
{children}
</DashboardContext.Provider>
);
}
// 订单列表组件
function OrderList() {
const { orders, loading } = useContext(DashboardContext);
// 问题:即使我只关心orders,但filters变化时,这个组件也会重新渲染
return (
<div>
{orders.map(order => <OrderCard key={order.id} order={order} />)}
</div>
);
}
// 统计卡片组件
function StatCard() {
const { orders } = useContext(DashboardContext);
// 这个组件也会重新渲染,即使它不关心filters
return<div>总订单数: {orders.length}</div>;
}
问题在哪? 让我画个图:
用户点击"最近30天"
↓
filters状态改变
↓
触发useEffect → 请求API
↓
orders状态改变
↓
Context的value对象重新创建
↓
┌─────────┬─────────┬─────────┬─────────┐
↓ ↓ ↓ ↓ ↓
订单列表 统计卡片 筛选器 用户头像 侧边栏
(需要) (需要) (不需要) (不需要) (不需要)
结果:5个组件全部重新渲染,但实际上只有2个需要
更糟糕的是,如果你在多个地方使用了useEffect监听状态变化:
// 组件A里
useEffect(() => {
console.log('filters变化了,我要做点什么');
}, [filters]);
// 组件B里
useEffect(() => {
console.log('filters变化了,我也要做点什么');
}, [filters]);
// 组件C里
useEffect(() => {
console.log('filters变化了,我还要做点什么');
}, [filters]);
用户只是点了一下按钮,结果触发了一堆连锁反应,就像推倒了多米诺骨牌。
很多同学(包括当时的我)第一反应是:加useMemo不就行了?
const contextValue = useMemo(() => ({
userInfo, orders, filters, loading,
setFilters, setUserInfo
}), [userInfo, orders, filters, loading]);
但这就像你在高速上堵车,你说:"我换个车道就能快了吧?"
问题是:如果整条高速都在堵,换车道有用吗?
useMemo的作用是"如果数据没变,就不重新计算"。但问题是:
useMemo只是让你少算几次,但它改变不了"哪些东西会变化"这个根本问题。
就像这样:
没有useMemo的情况:
用户点击 → 重新渲染5次 → 卡顿
加了useMemo的情况:
用户点击 → 重新渲染4次 → 还是卡顿(只是好了一点点)
理想情况:
用户点击 → 重新渲染1次 → 流畅
要达到"理想情况",需要从根本上改变数据管理的方式。
我经常用厨房来比喻React:
React就像一个高级厨师,他的强项是"给我食材,我做出美味的菜"。 但你不能让厨师去管采购、库存、供应商关系——那应该是采购部门(状态管理层)的工作。
把这个比喻对应到代码:
厨师(React)擅长的 | 厨师不擅长的 |
|---|---|
炒菜(渲染界面) | 管理食材库存(管理复杂状态) |
根据食材调整做法(响应数据变化) | 协调多个供应商(合并多个数据源) |
快速出菜(高效渲染) | 决定什么时候进货(控制API请求时机) |
举个实际例子:
假设你要做一个电商订单详情页:
用React的useState + useEffect来管理这些,就像让厨师自己去管供应链:
function OrderDetail() {
const [order, setOrder] = useState(null);
const [user, setUser] = useState(null);
const [logistics, setLogistics] = useState(null);
const [loading1, setLoading1] = useState(false);
const [loading2, setLoading2] = useState(false);
const [loading3, setLoading3] = useState(false);
const [error1, setError1] = useState(null);
const [error2, setError2] = useState(null);
const [error3, setError3] = useState(null);
useEffect(() => {
setLoading1(true);
fetch(`/api/order/${orderId}`)
.then(res => {
setOrder(res);
setLoading1(false);
// 拿到订单后,再请求用户信息
setLoading2(true);
fetch(`/api/user/${res.userId}`)
.then(userRes => {
setUser(userRes);
setLoading2(false);
})
.catch(err => {
setError2(err);
setLoading2(false);
});
// 同时请求物流信息
setLoading3(true);
fetch(`/api/logistics/${res.orderNo}`)
.then(logRes => {
setLogistics(logRes);
setLoading3(false);
})
.catch(err => {
setError3(err);
setLoading3(false);
});
})
.catch(err => {
setError1(err);
setLoading1(false);
});
}, [orderId]);
// 还要处理重试、超时、缓存...
// 代码越来越长,越来越难维护
}
看到了吗?代码嵌套3层,有9个状态变量,错误处理重复了3遍。
如果还要加上"失败重试"、"请求超时"、"缓存结果",代码会更乱。
**这就是我说的"让React做了它不擅长的事"**。
在尝试了各种状态管理库后,我最终选择了RxJS。不是因为它最流行,而是因为它的思路特别适合解决我遇到的问题。
我用水管系统来解释:
想象你家里有个自来水系统:
水源(数据源)
↓
水管(Observable)
↓ ← 这里可以加过滤器
↓ ← 这里可以加加热器
↓ ← 这里可以加分流器
水龙头(组件订阅)
在这个比喻里:
关键特点:
React思维(传统做法):
"我有一个状态,当状态变化时,组件会重新渲染。
我要用useEffect监听变化,然后做各种事情。"
Observable思维(新做法):
"我有一个数据流(像水管),我可以精确控制:
- 什么时候流动(防抖、节流)
- 怎么处理数据(过滤、转换)
- 怎么组合多个流(合并、拼接)
React组件只需要在需要的时候'打开水龙头'(订阅)"
场景:用户在搜索框输入,要实时搜索
React的传统做法:
function SearchBox() {
const [keyword, setKeyword] = useState('');
const [results, setResults] = useState([]);
useEffect(() => {
// 问题:用户每输入一个字就请求一次
// 用户输入"iPhone" → 请求6次(i, iP, iPh, iPho, iPhon, iPhone)
fetch(`/api/search?q=${keyword}`)
.then(res => setResults(res));
}, [keyword]);
return<input onChange={(e) => setKeyword(e.target.value)} />;
}
Observable的做法:
// 1. 先建立一个"数据流水管"
const keyword$ = new Subject(); // 这是水源
// 2. 在水管上装"设备"来处理数据
const searchResults$ = keyword$.pipe(
debounceTime(300), // 装一个"缓冲器":300ms内不重复流
distinctUntilChanged(), // 装一个"去重器":相同的不流
switchMap(keyword => // 装一个"切换器":来新的就取消旧的
from(fetch(`/api/search?q=${keyword}`))
),
shareReplay(1) // 装一个"储水器":缓存最新结果
);
// 3. React组件"打开水龙头"
function SearchBox() {
const [keyword, setKeyword] = useState('');
const results = useObservable(searchResults$, []);
const handleChange = (e) => {
const value = e.target.value;
setKeyword(value);
keyword$.next(value); // 往水管里注入新数据
};
return<input value={keyword} onChange={handleChange} />;
}
效果对比:
用户输入"iPhone"
React做法:
i → 请求/api/search?q=i
iP → 请求/api/search?q=iP
iPh → 请求/api/search?q=iPh
iPho → 请求/api/search?q=iPho
iPhon → 请求/api/search?q=iPhon
iPhone→ 请求/api/search?q=iPhone
总共:6次请求
Observable做法:
i → (等待300ms...)
iP → (等待300ms...)
iPh → (等待300ms...)
iPho → (等待300ms...)
iPhon → (等待300ms...)
iPhone→ (等待300ms...)→ 请求/api/search?q=iPhone
总共:1次请求
看到区别了吗?通过在"水管"上装"设备",我们精确控制了数据如何流动。
回到我们开头的订单看板项目,让我展示如何用Observable"搭建水管系统"。
改造前的问题:
改造后的思路:
// stores/dashboard.store.js
// 这个文件就是我们的"水管车间",负责搭建整个水管系统
import { BehaviorSubject, combineLatest, interval } from'rxjs';
import {
switchMap, // 切换器:来新的就取消旧的
map, // 转换器:改变数据形状
shareReplay, // 储水器:缓存最新值
distinctUntilChanged, // 去重器:真正变化才流动
debounceTime, // 缓冲器:延迟一段时间
catchError // 保险器:出错了怎么办
} from'rxjs/operators';
// === 第一步:建立基础"水源" ===
// 水源1:用户选择的筛选条件
// BehaviorSubject就像一个"带初始值的水源"
const filters$ = new BehaviorSubject({
dateRange: 'last7days', // 默认最近7天
channel: 'all' // 默认全部渠道
});
// 水源2:用户信息(只请求一次)
const userInfo$ = from(fetch('/api/user/me').then(r => r.json())).pipe(
shareReplay(1), // 缓存结果,后续订阅者直接用缓存,不重复请求
catchError(err =>of({ error: '用户信息获取失败' }))
);
// === 第二步:搭建"数据处理水管" ===
// 这根水管的作用:根据筛选条件自动获取订单数据
const orders$ = filters$.pipe(
// 1. 装一个"缓冲器":用户快速点击时,等300ms再请求
debounceTime(300),
// 2. 装一个"去重器":如果筛选条件没真正变化,就不往下流
distinctUntilChanged((prev, curr) =>
prev.dateRange === curr.dateRange && prev.channel === curr.channel
),
// 3. 装一个"切换器":
// 如果用户又点了新的筛选条件,就取消之前的请求
// 这样就不会有"请求竞争"的问题
switchMap(filters =>
from(
fetch(`/api/orders?date=${filters.dateRange}&channel=${filters.channel}`)
.then(r => r.json())
).pipe(
catchError(err =>of({ error: '订单数据获取失败' }))
)
),
// 4. 装一个"储水器":缓存最新的订单数据
shareReplay(1)
);
// === 第三步:组合多个水管 ===
// 用"三通接头"把多个水管接在一起
// 只有当所有水管都有水了,才往下流
exportconst dashboardState$ = combineLatest([
userInfo$, // 水管1:用户信息
orders$, // 水管2:订单数据
filters$ // 水管3:当前筛选条件
]).pipe(
// 把三个水管的水合并成一个对象
map(([userInfo, orders, filters]) => ({
userInfo,
orders,
filters,
loading: false
})),
shareReplay(1) // 缓存最终结果
);
// === 第四步:暴露"控制阀门"(外部可以控制水流) ===
// 更新筛选条件(往水源里注入新数据)
exportconst updateFilters = (newFilters) => {
const current = filters$.value; // 获取当前值
filters$.next({ ...current, ...newFilters }); // 注入新值
};
// 手动刷新(强制重新请求)
exportconst refreshData = () => {
const current = filters$.value;
filters$.next({ ...current }); // 即使值相同,也触发一次请求
};
// hooks/useObservable.js
// 这是一个通用的"水龙头",可以接到任何水管上
import { useEffect, useState } from'react';
function useObservable(observable$, initialValue) {
const [value, setValue] = useState(initialValue);
useEffect(() => {
// 打开水龙头(订阅)
const subscription = observable$.subscribe(setValue);
// 组件卸载时关掉水龙头(取消订阅)
return() => subscription.unsubscribe();
}, [observable$]);
return value;
}
// components/Dashboard.jsx
// 看看组件变得多简单!
import { useObservable } from'../hooks/useObservable';
import { dashboardState$, updateFilters } from'../stores/dashboard.store';
function Dashboard() {
// 打开水龙头,订阅数据流
const state = useObservable(dashboardState$, {
userInfo: null,
orders: [],
filters: {},
loading: true
});
return (
<div className="dashboard">
{/* 筛选栏 */}
<FilterBar
filters={state.filters}
onChange={updateFilters} // 直接调用控制函数
/>
{/* 订单列表 */}
<OrderList
orders={state.orders}
loading={state.loading}
/>
{/* 用户信息 */}
<UserPanel user={state.userInfo} />
</div>
);
}
// 如果某个子组件只需要订单数据,可以直接订阅orders$
function OrderCounter() {
const orders = useObservable(orders$, []);
return<div>总订单数: {orders.length}</div>;
}
对比一下代码量:
改造前:
DashboardProvider: 80行(各种useState、useEffect)
Dashboard组件: 50行(useContext、各种状态处理)
总计: 130行,而且逻辑分散
改造后:
dashboard.store.js: 60行(所有数据逻辑在这里)
Dashboard组件: 25行(只负责渲染)
总计: 85行,而且逻辑集中清晰
改造完成后,我用Chrome DevTools做了详细的性能对比测试。
**模拟操作:**用户在5秒内快速切换了3次筛选条件
改造前的惨状:
用户点击1 → API请求1发出
用户点击2 → API请求2发出(请求1还没回来)
用户点击3 → API请求3发出(请求1、2都还没回来)
请求1回来 → 触发渲染(但数据已经过时了)
请求2回来 → 再次渲染(数据还是过时)
请求3回来 → 又一次渲染(终于是对的数据)
结果:
- 发送了3个API请求
- 组件重新渲染了14次(包括子组件)
- 总耗时4.2秒
- 用户看到了2次"错误"的数据(请求1和2的结果)
改造后的流畅体验:
用户点击1 → (等待300ms...)
用户点击2 → (重新计时,等待300ms...)
用户点击3 → (重新计时,等待300ms...)→ API请求发出
请求回来 → 触发渲染(数据准确)
结果:
- 只发送了1个API请求(最终的那个)
- 组件只渲染了2次(初始+数据到达)
- 总耗时1.1秒
- 用户只看到了最终的正确数据
性能对比图:
耗时对比:
改造前: ████████████████████████ 4.2秒
改造后: ██████ 1.1秒
提升: 74% ⬆️
API请求数:
改造前: ███ 3次
改造后: █ 1次
减少: 67% ⬇️
组件渲染次数:
改造前: ██████████████ 14次
改造后: ██ 2次
减少: 86% ⬇️
改造前:
Timeline:
0ms ┬─ 组件挂载
├─ 发起请求1(用户信息)
├─ 发起请求2(订单数据)
└─ 发起请求3(统计数据)
200ms ├─ 请求1完成 → 渲染1次 → 5个子组件跟着渲染
300ms ├─ 请求2完成 → 渲染1次 → 5个子组件跟着渲染
400ms └─ 请求3完成 → 渲染1次 → 5个子组件跟着渲染
总渲染: 18次(主组件3次 + 子组件15次)
改造后:
Timeline:
0ms ┬─ 组件挂载,订阅数据流
├─ 数据流自动发起3个请求
(等待所有请求完成...)
400ms └─ 所有数据就绪 → 渲染1次 → 子组件跟着渲染1次
总渲染: 6次(主组件1次 + 子组件5次)
首次加载对比:
改造前:18次渲染 ████████████████████
改造后:6次渲染 ██████
减少: 67% ⬇️
改造前:
改造后:
以前添加新功能时,总担心:"会不会让应用更卡?"
改造后,新功能继承了Observable的优化能力:
举个例子: 后来我们加了一个"实时订单数"的功能,需要每5秒刷新一次。
// 只需要简单地加一个定时数据流
const realtimeCount$ = interval(5000).pipe(
switchMap(() => from(fetch('/api/orders/count'))),
shareReplay(1)
);
// React组件直接用
function OrderCount() {
const count = useObservable(realtimeCount$, 0);
return <span>{count}</span>;
}
这个新功能:
添加新功能从 "战战兢兢" 变成了 "信心满满" 。
经过这次改造,我总结出了几个重要经验。如果早点明白这些道理,就能少走很多弯路。
错误观念:"React提供了useState和useEffect,所以所有逻辑都应该写在组件里。"
正确认知:React就像一个展示柜,它擅长"摆放商品"(渲染UI),但不擅长"管理仓库"(复杂数据流)。
实际例子:
// ❌ 让React管太多事(不好的做法)
function ProductList() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
// 要管理5个状态,逻辑混在一起
useEffect(() => {
setLoading(true);
fetch(`/api/products?page=${page}`)
.then(res => {
setProducts(prev => [...prev, ...res.data]);
setHasMore(res.hasMore);
setLoading(false);
})
.catch(err => {
setError(err);
setLoading(false);
});
}, [page]);
// 还要处理重试、缓存、防抖...越写越乱
}
// ✅ 让专门的层管理数据(好的做法)
// 在store文件里
const products$ = pageNumber$.pipe(
switchMap(page =>
from(fetch(`/api/products?page=${page}`))
.pipe(
retry(2),
timeout(5000),
catchError(handleError)
)
),
scan((acc, curr) => [...acc, ...curr.data], []),
shareReplay(1)
);
// React组件只负责渲染
function ProductList() {
const products = useObservable(products$, []);
return products.map(p =><ProductCard key={p.id} product={p} />);
}
对比:
很多人遇到性能问题,第一反应就是"加个useMemo试试"。
但这就像你家里漏水,你却只想着拿桶接水,而不是修水管。
举个实际例子:
// ❌ 用useMemo"接水"(治标不治本)
function OrderList() {
const { orders } = useContext(OrderContext);
// 以为加了useMemo就快了
const sortedOrders = useMemo(() => {
return orders.sort((a, b) => b.amount - a.amount);
}, [orders]);
// 问题:如果orders每次都是新数组,useMemo就没用
// Context一更新 → 新的orders数组 → useMemo重新计算 → 还是慢
}
// ✅ 从源头"修水管"(治本)
// 在Observable层面就控制好何时更新
const sortedOrders$ = orders$.pipe(
distinctUntilChanged(isArrayEqual), // 数组内容真的变了才emit
map(orders => orders.sort((a, b) => b.amount - a.amount)),
shareReplay(1) // 缓存排序结果
);
function OrderList() {
const orders = useObservable(sortedOrders$, []);
// 不需要useMemo,因为Observable已经控制好了
}
关键区别:
之前的做法: 请求逻辑散落在各个组件的useEffect里
// ❌ 请求逻辑分散(电商订单详情页)
function OrderDetail() {
const [order, setOrder] = useState(null);
const [user, setUser] = useState(null);
const [logistics, setLogistics] = useState(null);
useEffect(() => {
// 请求1:订单信息
fetch(`/api/order/${orderId}`).then(setOrder);
}, [orderId]);
useEffect(() => {
// 请求2:等order有了,再请求用户
if (order) {
fetch(`/api/user/${order.userId}`).then(setUser);
}
}, [order]);
useEffect(() => {
// 请求3:等order有了,再请求物流
if (order) {
fetch(`/api/logistics/${order.orderNo}`).then(setLogistics);
}
}, [order]);
// 问题:
// 1. 请求逻辑分散在3个useEffect
// 2. 有依赖关系但不清晰
// 3. 错误处理要写3遍
// 4. 无法方便地重试或取消
}
改造后: 请求逻辑集中管理,流程一目了然
// ✅ Observable清晰描述请求流程
const orderDetail$ = orderId$.pipe(
switchMap(orderId =>
// 第一步:获取订单
from(fetch(`/api/order/${orderId}`).then(r => r.json())).pipe(
// 第二步:根据订单同时获取用户和物流
switchMap(order =>
combineLatest([
of(order), // 保留订单数据
from(fetch(`/api/user/${order.userId}`).then(r => r.json())),
from(fetch(`/api/logistics/${order.orderNo}`).then(r => r.json()))
])
)
)
),
map(([order, user, logistics]) => ({ order, user, logistics })),
retry(2), // 统一的重试逻辑
catchError(handleError), // 统一的错误处理
shareReplay(1)
);
// React组件超级简单
function OrderDetail() {
const detail = useObservable(orderDetail$, null);
if (!detail) return<Loading />;
return (
<div>
<OrderInfo order={detail.order} />
<UserInfo user={detail.user} />
<LogisticsInfo logistics={detail.logistics} />
</div>
);
}
对比:
好的代码应该一眼就能看出"数据怎么流动"。
反例:
// ❌ 看不出数据流向
useEffect(() => { fetchA(); }, [x]);
useEffect(() => { fetchB(); }, [y]);
useEffect(() => { fetchC(); }, [x, y]);
// 要看完3个useEffect才知道谁依赖谁
正例:
// ✅ 数据流向一目了然
const result$ = combineLatest([
sourceA$,
sourceB$.pipe(debounceTime(500)),
sourceC$.pipe(distinctUntilChanged())
]).pipe(
map(([a, b, c]) => compute(a, b, c)),
shareReplay(1)
);
// 一看就知道:result依赖三个源,sourceB有防抖,sourceC有去重
生活比喻:
Observable就像一张"水管施工图",一眼就能看出:
而useState + useEffect就像"口头描述",你得听很久才能理解。
React性能问题,90%都是"不该更新的时候更新了"。
关键是:你能精确控制什么时候更新吗?
useState的问题:
const [data, setData] = useState([]);
// 无法精确控制:
// - 什么时候应该更新?
// - 什么情况下不更新?
// - 如何防止重复更新?
setData(newData); // 只能"设置",无法"控制"
Observable的优势:
const data$ = source$.pipe(
debounceTime(300), // 控制:300ms内不重复更新
distinctUntilChanged(), // 控制:真正变化才更新
throttleTime(1000), // 控制:1秒最多更新1次
sample(trigger$), // 控制:只在特定时机更新
shareReplay(1) // 控制:缓存,避免重复计算
);
// 每一行都在"控制"数据如何流动
实际效果:
没有控制的数据流:
用户快速点击 → 产生10次更新 → 卡顿
精确控制的数据流:
用户快速点击 → 防抖后只更新1次 → 流畅
如果你的项目已经存在,不建议一次性全部重构。我们团队是这样做的:
使用React DevTools Profiler找到re-render最频繁的组件,通常是:
先把一些独立的数据获取逻辑改造成Observable:
// 原来
asyncfunction fetchUserMetrics(userId) {
const res = await fetch(`/api/metrics/${userId}`);
return res.json();
}
// 改造
function createUserMetrics$(userId$) {
return userId$.pipe(
switchMap(id =>from(fetch(`/api/metrics/${id}`))),
map(res => res.json()),
shareReplay(1)
);
}
为核心业务模块建立独立的Store,与React解耦:
// stores/order.store.js
export const orderState$ = combineLatest([...]).pipe(...);
export const updateOrder = (id, data) => {...};
export const cancelOrder = (id) => {...};
只在必要的地方订阅Observable,其他地方保持原样。
从去年重构到现在,这个数据看板项目已经稳定运行了好几个月。性能问题基本解决了,用户体验也好了不少,团队在添加新功能时也不用担心会影响整体性能。
React依然是很好的UI框架,但在复杂的异步数据流场景下,确实需要专门的状态管理方案。认清工具的适用范围,在合适的场景下选择合适的方案,这是我这次重构最大的收获。
如果你的React项目也遇到类似的性能问题,可以考虑引入Observable的思路。当然,具体要不要用RxJS还是要看项目实际情况,毕竟引入新技术也有学习成本。
如果这篇文章对你有帮助,欢迎关注《前端达人》,会持续分享React、前端架构等实战经验。
觉得有用的话点个赞,让更多人看到 👍