
如果你写React超过1年,一定遇到过这种情况:
项目刚开始,代码跑得飞快。几个月后,页面开始有点"粘",滚动不够丝滑,输入框偶尔卡顿,列表更新有明显延迟。你打开DevTools看了看,网络请求正常,接口响应也快,浏览器性能也没问题。
但就是慢。
这种"慢"很难定位,因为它不是某个明确的bug,而是无数个"看起来没问题"的小决策累积出来的结果。
大部分React教程会告诉你useState怎么用、useEffect怎么写,但几乎没人告诉你:在真实的业务场景中,让你的React卡顿的,往往不是你写了什么,而是你没优化什么。
今天我们聊聊那3个被严重低估的Hook,以及它们是如何在大厂项目中拯救性能的。
先说一个很多人都有的误解:React慢是因为虚拟DOM太重了。
错。
React变慢,99%的情况是因为你让它做了太多不必要的重渲染。
理解这个逻辑链条:
组件重新渲染
→ 所有Hook重新执行
→ 虚拟DOM重新构建
→ React做Diff对比
→ 浏览器重新绘制
→ 用户感受到卡顿
看起来这是React的正常工作流程,但问题在于:大部分重渲染根本不需要发生。
假设你在做一个企业级后台系统(比如类似钉钉那种),有个用户列表页面:
function UserList() {
const [users, setUsers] = useState([]);
const [selected, setSelected] = useState(null);
// 每次渲染都会创建新的函数
const handleSelect = (id) => {
setSelected(id);
};
// 每次渲染都会重新计算
const sortedUsers = users
.filter(user => user.isActive)
.sort((a, b) => a.name.localeCompare(b.name));
return (
<div>
{sortedUsers.map(user => (
<UserCard
key={user.id}
user={user}
onSelect={handleSelect} // 🔴 问题在这里
/>
))}
</div>
);
}
这段代码看起来完全正常,对吧?
但在有500个用户的列表中,只要selected状态改变一次:
handleSelect函数会被重新创建sortedUsers会被重新计算UserCard组件都会重新渲染因为React认为:函数变了 = props变了 = 需要重新渲染。
这就是问题的根源:引用的不稳定性(Reference Instability)。
很多人以为useCallback是用来"提升性能"的。
错了。
useCallback真正的作用是:保持函数引用的稳定性,防止子组件不必要的重渲染。
function UserList() {
const [selected, setSelected] = useState(null);
// ✅ 函数引用稳定,只有依赖项变化时才重新创建
const handleSelect = useCallback((id) => {
setSelected(id);
}, []); // 空依赖数组 = 组件生命周期内只创建一次
return (
<div>
{users.map(user => (
<UserCard
key={user.id}
user={user}
onSelect={handleSelect} // ✅ 引用稳定,子组件不会无谓重渲染
/>
))}
</div>
);
}
在JavaScript中,每次函数重新声明,都会在内存中创建一个新的对象:
// 每次render
const fn1 = () => {}; // 内存地址: 0x001
// 下次render
const fn2 = () => {}; // 内存地址: 0x002
fn1 === fn2 // false - React认为这是"不同"的props
useCallback做的事情就是:
const fn = useCallback(() => {}, []);
// 第一次: 0x001
// 第二次: 0x001 ← 还是同一个
// 第三次: 0x001 ← 还是同一个
这不是性能优化,这是设计模式 —— 保持引用的稳定性。
如果说useCallback是稳定函数引用,那useMemo就是缓存计算结果。
// ❌ 错误示范:每次render都重新计算
function Dashboard() {
const data = fetchData();
// 假设这是个复杂的数据处理
const processed = data
.filter(item => item.status === 'active')
.map(item => ({
...item,
score: calculateComplexScore(item) // 假设这个计算很重
}))
.sort((a, b) => b.score - a.score)
.slice(0, 10);
return<Chart data={processed} />;
}
如果Dashboard因为其他state变化而重新渲染,即使data完全没变,这个复杂的数据处理也会重新执行一遍。
function Dashboard() {
const data = fetchData();
// ✅ 只有data变化时才重新计算
const processed = useMemo(() => {
return data
.filter(item => item.status === 'active')
.map(item => ({
...item,
score: calculateComplexScore(item)
}))
.sort((a, b) => b.score - a.score)
.slice(0, 10);
}, [data]); // 只依赖data
return<Chart data={processed} />;
}
useMemo告诉React:
"只有当依赖项真正变化时,才重新计算这个值。否则,用上次的结果就行。"
这不是偷懒,这是避免浪费CPU资源。
优化前:
优化后:
const filteredProducts = useMemo(() => {
return products.filter(product => {
return (
(!filters.category || product.category === filters.category) &&
(!filters.minPrice || product.price >= filters.minPrice) &&
(!filters.maxPrice || product.price <= filters.maxPrice) &&
// ... 更多筛选条件
);
});
}, [products, filters]); // 只有products或filters变化时才重新筛选
结果:
useRef是最被低估的Hook之一。
很多人以为它只是用来"获取DOM元素"的,但实际上,它是存储"不需要触发UI更新的值"的完美容器。
function ScrollList() {
const [items, setItems] = useState([]);
const lastScrollY = useRef(0); // ✅ 不会触发重渲染
useEffect(() => {
const handleScroll = () => {
const currentY = window.scrollY;
// 记录滚动位置,但不需要触发UI更新
lastScrollY.current = currentY;
// 只在特定条件下才更新state(触发渲染)
if (currentY > lastScrollY.current + 500) {
loadMoreItems();
}
};
window.addEventListener('scroll', handleScroll);
return() =>window.removeEventListener('scroll', handleScroll);
}, []);
return<div>{/* 列表内容 */}</div>;
}
如果用useState存储lastScrollY,每次滚动都会触发重渲染,性能直接崩溃。
// 1. 存储定时器ID
const timerId = useRef(null);
timerId.current = setTimeout(() => {
// do something
}, 1000);
// 2. 存储上一次的值
const prevValue = useRef(null);
useEffect(() => {
prevValue.current = currentValue;
}, [currentValue]);
// 3. 存储不需要响应式的配置
const config = useRef({
threshold: 100,
debounceTime: 300
});
// 4. 避免闭包陷阱
const latestCallback = useRef(callback);
latestCallback.current = callback; // 每次更新最新值
这是我们在某金融项目中遇到的真实case:
场景:实时行情列表页面
function StockList({ stocks }) {
const [selected, setSelected] = useState(null);
return (
<div>
{stocks.map(stock => (
<StockCard
key={stock.id}
stock={stock}
onSelect={(id) => setSelected(id)} // 🔴 每次render创建新函数
isSelected={selected === stock.id}
/>
))}
</div>
);
}
function StockCard({ stock, onSelect, isSelected }) {
// 🔴 每次render都重新计算
const changePercent = ((stock.current - stock.prev) / stock.prev * 100).toFixed(2);
const trendColor = changePercent > 0 ? 'red' : 'green';
return (
<div onClick={() => onSelect(stock.id)}>
<span style={{ color: trendColor }}>
{changePercent}%
</span>
</div>
);
}
问题分析:
StockCard重渲染changePercentfunction StockList({ stocks }) {
const [selected, setSelected] = useState(null);
// ✅ 稳定的函数引用
const handleSelect = useCallback((id) => {
setSelected(id);
}, []);
// ✅ 只在stocks变化时重新计算
const sortedStocks = useMemo(() => {
return [...stocks].sort((a, b) => b.volume - a.volume);
}, [stocks]);
return (
<div>
{sortedStocks.map(stock => (
<MemoizedStockCard
key={stock.id}
stock={stock}
onSelect={handleSelect} // ✅ 引用稳定
isSelected={selected === stock.id}
/>
))}
</div>
);
}
// ✅ 使用React.memo防止不必要的重渲染
const MemoizedStockCard = React.memo(function StockCard({ stock, onSelect, isSelected }) {
// ✅ 只在stock变化时重新计算
const stats = useMemo(() => {
const changePercent = ((stock.current - stock.prev) / stock.prev * 100).toFixed(2);
const trendColor = changePercent > 0 ? 'red' : 'green';
return { changePercent, trendColor };
}, [stock]);
return (
<div onClick={() => onSelect(stock.id)}>
<span style={{ color: stats.trendColor }}>
{stats.changePercent}%
</span>
</div>
);
});
指标 | 优化前 | 优化后 |
|---|---|---|
滚动时每秒渲染次数 | ~280次 | ~14次 |
首次交互时间 | 4.2s | 1.9s |
用户体感 | 明显卡顿 | 接近原生流畅 |
关键收益:
在大厂工作过一段时间后,你会发现一个规律:
优秀的React代码不是靠"优化"出来的,而是靠"设计"出来的。
useMemo/useCallback定义了"什么时候应该重新计算"很多人觉得React性能优化是在和框架做斗争,其实不是。
React的设计哲学是:给你足够的灵活性,但需要你有意识地控制复杂度。
当我们理解了这一点:
你的React应用自然就会变快、变稳、变得可预测。
不是因为你学会了什么黑魔法,而是因为你开始像架构师一样思考渲染边界,而不只是像搬砖工一样堆组件。
最后一句话送给每个React开发者:
好的性能不是优化出来的,是设计出来的。当你开始思考"这个组件真的需要重渲染吗"时,你就已经迈出了从初级到高级的第一步。