超越手动实现的 memo 的 React 性能:结构与调度机制
编译器时代的建议:将状态集中管理,区分紧急任务,减少JS代码输出,再对检测到的高频调用点进行缓存。
多年来,关于 React 性能优化的建议一直都是将所有内容用 memo API 包装:
const value = useMemo(() => expensiveCalculation(data), [data]);
const handleClick = useCallback(() => {
doSomething(id);
}, [id]);export default React.memo(Component);
function ProductList({ products, onSelect }) {
const processed = useMemo(
() => processProducts(products),
[products]
);
const handleSelect = useCallback(
(id) => onSelect(id),
[onSelect]
); return (
<List
products={processed}
onSelect={handleSelect}
/>
);
}
App
│
├── Header
├── Sidebar
├── Search
├── ProductList
└── Footer
function App() {
const [query, setQuery] = useState("");
// large component tree
}
App
│
├── Header
├── Sidebar
├── Search
│ └── query state
├── ProductList
└── Footer
const [query, setQuery] = useState("");
const [isPending, startTransition] = useTransition();
function handleChange(event) {
const value = event.target.value; setQuery(value); startTransition(() => {
setSearchResults(filterProducts(value));
});
}
User input
│
├── urgent ───────► keep UI responsive
│
└── non-urgent ───► transition
const AnalyticsDashboard = lazy(
() => import("./AnalyticsDashboard")
);
Initial JavaScript
│
▼
┌─────────────────────┐
│ Everything │
│ Dashboard │
│ Analytics │
│ Editor │
│ Admin │
└─────────────────────┘
Initial load
│
├── Core UI
│
└── Later
├── Analytics
├── Editor
└── Admin
"This component renders often."
│
▼
Add useMemo
"This component renders often."
│
▼
Why?
│
┌──────┼───────────┐
▼ ▼ ▼
State Expensive Large
flow work subtree
│ │ │
▼ ▼ ▼
Colocate Optimize Restructure
1. Measure
↓
2. Find the expensive work
↓
3. Fix component architecture
↓
4. Reduce unnecessary JavaScript
↓
5. Prioritize updates correctly
↓
6. Let React Compiler handle memoization
↓
7. Manually optimize only when evidence says you should
仅靠记忆化并不能解决应用运行缓慢的问题。即使树结构已经完全实现记忆化,仍可能因状态过大、紧急更新却执行非紧急任务,或是主线程上存在大量 JavaScript 代码而导致性能问题。
React 编译器改变了这一默认做法
编译器自动处理了许多记忆化相关的工作,这样人们就可以将精力集中在代码结构和调度上,而无需再到处手动添加 useMemo。
第一步:将状态放在更接近使用它的地方
树中的高状态会重新渲染整个子树。将状态与需要它的叶子节点放在一起;只有在共享时有必要才进行提升操作。
第二:不要让紧急更新去处理非紧急任务
保持输入和动画的响应性。通过过渡效果或延迟值来推迟非紧急任务,避免按键操作被耗时的过滤器阻塞。
第三:审视你发布的 JavaScript 代码
在 React 的协调器发挥作用之前,庞大的同步循环、耗时的格式化函数以及无限制的列表就已经占据了主导地位。应对 JavaScript 代码进行性能分析,而不仅仅是依赖 React DevTools 的提示。
那么何时应该使用 useMemo 呢?
在子组件因属性变化而失效时保持引用稳定性,以及在进行真正耗时的纯计算之后——在经过测量之后——它仍然很有用。但它并非适用于所有值的默认解决方案。
React 的新性能思维方式
结构 → 调度 → 减少 JavaScript 的使用量 → 再对高频访问区域进行微缓存。编译器辅助的缓存功能只是辅助工具,不能替代良好的设计。
在代码提交中需设定性能预算:主要表单的交互到下一次重绘的时间,以及已知性能较差路径的 React 性能分析快照。
列表应在对每一行进行缓存之前先实现虚拟化处理。即便对 10,000 行数据进行了缓存,依然无法提升性能。
在代码提交中需设定性能预算:主要表单的交互到下一次重绘的时间,以及已知性能较差路径的 React 性能分析快照。
列表应在对每一行进行缓存之前先实现虚拟化处理。即便对 10,000 行数据进行了缓存,依然无法提升性能。
在代码提交中需设定性能预算:主要表单的交互到下一次重绘的时间,以及已知性能较差路径的 React 性能分析快照。
列表应在对每一行进行缓存之前先实现虚拟化处理。即便对 10,000 行数据进行了缓存,依然无法提升性能。
为拉取请求设定性能预算:主表单需测量到下一次重绘的交互时间,同时针对已知性能较差的路由生成 React Profiler 的快照。
列表应在对每一行进行缓存之前先实现虚拟化处理。即便在包含1万行的页面加载时使用缓存,性能依然会不佳。
为拉取请求设定性能预算:主表单需测量到下一次重绘的交互时间,同时针对已知性能较差的路由生成 React Profiler 的快照。
列表应在对每一行进行缓存之前先实现虚拟化处理。即便在包含1万行的页面加载时使用缓存,性能依然会不佳。
为拉取请求设定性能预算:主表单需测量到下一次重绘的交互时间,同时针对已知性能较差的路由生成 React Profiler 的快照。
列表应在对每一行进行缓存之前先实现虚拟化处理。即便在包含1万行的页面加载时使用缓存,性能依然会不佳。