译:不要过度优化你的优化

新开发者经常听到或读到 useCallbackuseMemo 是提高 React 应用程序 性能 的工具。

因此,他们开始在任何地方使用它们,希望优化他们的应用程序。但这通常会导致代码混乱、过于冗长,充满复杂的依赖数组------讽刺的是,这往往弊大于利

在这篇文章中,我们将探讨 useCallbackuseMemo 实际上做什么,何时它们有用,以及------更重要的是------何时避免使用它们。

让我们了解一下 useCallbackuseMemo 的作用:

useCallback: "记住"你传递给它的函数,这样 React 就不会在每次组件更新时创建该函数的全新版本。

tsx 复制代码
const handleClick = useCallback(() => {
  console.log("Button clicked");
}, []);

在上面的代码片段中,只要依赖数组([])不改变,handleClick 在渲染过程中将保持不变。如果没有 useCallback,每次渲染都会创建一个新函数。

useMemo: 让 React 记住(或"缓存")计算的结果,这样它就不会在每次组件更新时重新计算。

tsx 复制代码
const expensiveValue = useMemo(() => {
  return computeHeavyValue(input);
}, [input]);

在这里,computeHeavyValue(input) 只在 input 改变时运行。如果在下次渲染时 input 相同,React 会重用缓存的结果而不是重新计算。

做这些事情(当 props 改变时记住你的值)是有代价的。

useCallbackuseMemo 的隐藏成本

使用 useCallback 和 useMemo 背后的核心思想是记忆化并不是免费的------这是一种权衡。你花费额外的 CPU 周期和内存来缓存函数或值,希望在未来的渲染中节省更昂贵的计算。但如果使用不当,记忆化的成本可能会超过收益。

1. 内存开销: 这是最直接的成本。当你使用 useCallbackuseMemo 时,React 必须在渲染之间将记忆化的函数或值存储在内存中。

2. CPU 开销: 在 React 决定是使用缓存值还是重新运行函数之前,React 必须遍历依赖数组并对每个项目与上一次渲染的值进行浅比较。

3. 可读性和维护性: 记忆化每个函数或值会创建冗长的代码,难以阅读和理解。这些 hooks 强制开发者仔细管理依赖数组。忘记或添加新的依赖可能会创建微妙的 bug。

tsx 复制代码
const memoizedFn = useCallback(() => {
  doSomething(a, b);
}, [a]); // 依赖中缺少 b --- 可能导致 bug

何时不要优化

让我们看一个使用 useCallback 不必要的情况:

tsx 复制代码
const logEvent = useCallback(() => {
  analytics.track('button_click');
}, []);

<button onClick={logEvent}>Click me</button>

在这个例子中,logEvent 函数很简单,并且没有传递给任何 记忆化的子组件

因此,使用 useCallback 没有提供真正的好处,只是增加了不必要的复杂性。

tsx 复制代码
const UserInput = React.memo(({ onChange }) => {
  const handleChange = useCallback(event => {
    onChange(event.target.value);
  }, [onChange]);

  return <input onChange={handleChange} />;
});

在上面的例子中,handleChange 总是基于 onChange 创建,这是唯一的 prop,所以当父组件重新渲染并传递新的 onChange prop 时,React.memouseCallback 都会触发重新渲染并重新创建 handleChange。如果 onChange 没有改变,React.memo 会阻止重新渲染,所以 handleChange 也不会重新计算------即使没有 useCallback。所以这里 useCallback 没有被正确使用。

tsx 复制代码
const _debounce = (func, delay) => { /* returns debounced function */ };

const makeApiCall = (e) => { console.log("Making an API call"); };

const debounce = useCallback(_debounce(makeApiCall, 2000), []);

<input onChange={e => debounce(e.target.value)} />

尽管使用了 useCallback,函数每次都会被重新定义(因为函数调用是内联的),所以所有的记忆化都是无意义的,并增加了 CPU 成本和内存使用。

现在让我们探讨使用 useMemo 不必要的情况:

  • 记忆化小计算
tsx 复制代码
const filteredTodos = useMemo(() => {
  return todos.filter(todo => todo.completed);
}, [todos]);

为什么这是过度的:这里的 .filter 非常快,内联使用它也会更具可读性。useMemo 在这里增加了内存开销和依赖复杂性。

  • 记忆化 JSX 树
tsx 复制代码
const todoRows = useMemo(() => {
  return todos.map(todo => <ToDoRow key={todo.id} todo={todo} />);
}, [todos]);

useMemo 只是存储一个值------但 React 仍然会重新渲染每个子组件,除非它们用 React.memo 记忆化。

何时优化

useCallback、useMemo 可以是强大的工具------但只有在正确的情况下使用时。

  • 使用 useMemo 优化昂贵的计算
tsx 复制代码
const filteredUsers = useMemo(() => {
  // 昂贵的过滤逻辑
  return users.filter(user => user.name.toLowerCase().includes(searchTerm.toLowerCase()));
}, [users, searchTerm]);

在上面的例子中,useMemo 记忆化了过滤后的用户列表,所以过滤函数只有在 users 数组或 searchTerm 改变时才会再次运行。这种优化可以通过避免每次渲染时的不必要重新计算来显著提高性能,即使在大数据集的情况下也能保持 UI 响应性。

  • 使用 useCallback 为子组件提供稳定的回调
tsx 复制代码
const handleUserSelect = useCallback((userId) => {
  // 选择用户的逻辑
  setSelectedUserId(userId);
}, [setSelectedUserId]);

在这个例子中,useCallback 记忆化了 handleUserSelect 函数,使其引用只有在 setSelectedUserId 改变时才会改变。这种稳定性防止依赖 handleUserSelect 的子组件重新渲染,除非真正必要,提高了渲染效率和整体应用性能。

  • useCallback 和 useMemo 一起用于关联操作
tsx 复制代码
const filteredUsers = useMemo(() => {
  return users.filter(user => user.name.toLowerCase().includes(searchTerm.toLowerCase()));
}, [users, searchTerm]);

const handleUserSelect = useCallback((userId) => {
  setSelectedUserId(userId);
}, [setSelectedUserId]);

return (
  <UserList users={filteredUsers} onUserSelect={handleUserSelect} />
);

useMemo 记忆化过滤后的用户列表,避免在每次渲染时重新计算过滤数组,除非 users 或 searchTerm 改变。useCallback 记忆化 handleUserSelect 函数以保持其引用稳定,防止接收它作为 prop 的子组件不必要的重新渲染。

通过结合这两个 hooks,你确保子组件 <UserList> 接收到稳定的数据和稳定的回调,这有助于 React 跳过不必要的渲染,提高性能并保持 UI 响应性。

总结

useCallbackuseMemo 是强大的 hooks,可以提升 React 应用性能,但只有在深思熟虑地应用时才有效。过度使用它们会导致复杂、难以维护的代码,有时甚至会带来更差的性能。

总是问自己:

  • 函数或值的计算是否昂贵?

  • 它是否传递给依赖稳定引用的记忆化子组件?

  • 记忆化是否会有意义地减少不必要的渲染或重新计算?

如果答案是肯定的,useCallbackuseMemo 可以是很好的盟友。如果不是,保持代码简单清晰通常更好地为你服务。

记住,过早优化 是万恶之源------只有在你知道问题存在时才进行优化!

相关推荐
Sammyyyyy5 分钟前
2025年,Javascript后端应该用 Bun、Node.js 还是 Deno?
开发语言·javascript·node.js
timeweaver15 分钟前
深度解析 Nginx 前端 location 配置与优先级:你真的用对了吗?
前端·nginx·前端工程化
鲸落落丶16 分钟前
网络通信---Axios
前端
wwy_frontend17 分钟前
React性能优化实战:从卡顿到丝滑的8个技巧
前端·react.js
小高00733 分钟前
面试官:npm run build 到底干了什么?从 package.json 到 dist 的 7 步拆解
前端·javascript·vue.js
天选打工圣体33 分钟前
个人学习笔记总结(四)抽离elpis并发布npm包
前端
wayhome在哪43 分钟前
用 fabric.js 搞定电子签名拖拽合成图片
javascript·产品·canvas
JayceM2 小时前
Vue中v-show与v-if的区别
前端·javascript·vue.js
HWL56792 小时前
“preinstall“: “npx only-allow pnpm“
运维·服务器·前端·javascript·vue.js
咪咪渝粮2 小时前
JavaScript 中constructor 属性的指向异常问题
开发语言·javascript