news 2026/9/15 12:31:31

React 性能优化实战指南:useRef、useMemo、useCallback 与 React Compiler 全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
React 性能优化实战指南:useRef、useMemo、useCallback 与 React Compiler 全解析

React 性能优化实战指南:useRef、useMemo、useCallback 与 React Compiler 全解析

【免费下载链接】curriculumThe open curriculum for learning web development项目地址: https://gitcode.com/GitHub_Trending/cu/curriculum

本指南以 React 课程中 refs_and_memoization.md 一课为骨架,系统讲解在 React 声明式模型下如何通过useRef完成命令式 DOM 操作、如何用useMemo/useCallbackmemo做引用级性能优化,并介绍 React Compiler 的自动记忆化能力。读完本文,你将能独立判断"何时该用 Ref、何时该记忆化、何时什么都不用做",并能在购物车、计数器等真实组件中落地这些技巧。

引言:React 声明式模型下的两个难题

在纯 JavaScript 时代,我们习惯用querySelectoraddEventListener直接操作 DOM。但 React 是声明式的——组件描述"UI 应该长什么样",由 React 负责把变化提交(commit)到 DOM。于是产生了两个问题:

  1. 当我们确实需要像原生 JS 那样直接操作某个 DOM 元素时(聚焦、测量尺寸、滚动),该怎么做?
  2. 每当 state 发生变化,React 会重新渲染组件,意味着组件内所有不受 React 控制的局部变量都会被销毁并重新执行——如果里面有一段非常昂贵的计算,每次重渲染都重跑一遍,应用会不会变慢?

这两个问题分别对应两类解决方案:Refs(引用)Memoization(记忆化)

理解 React 的重渲染心智模型

在深入优化手段之前,先建立正确的心智模型。正如 introduction_to_state.md 中所述:

当组件的 state 或 props 变化时,React 会从头再次执行组件函数,以确定基于最新 state 和 props 应该显示什么,然后将计算出的变更应用到 DOM(提交)。整个过程称为重渲染(re-rendering)

这意味着每次重渲染,组件内的普通函数、变量都会被"销毁并重新执行";只有useState等 React 管理的东西会保留最新值。这恰好说明了一个关键事实:组件本质上就是一个函数,函数体内的逻辑会在每次调用时重新运行。

同时,React 的重渲染会生成新的虚拟 DOM 树,通过reconciliation(协调)算法与旧树对比,计算出更新真实 DOM 所需的最小变更集。这也是为什么"state 变化会连带重渲染子组件"——父组件函数重新执行时,子组件元素被重新创建。

useRef:管理不参与渲染的值

useRef 是什么

useRef钩子让你管理一个不需要用于渲染的值。它是 state 的替代方案:当你希望组件"记住"某些信息,但又不想让这些信息触发新的渲染时,就可以使用它。

它的核心特性:

  • 跨渲染持久化:ref 的值在整个组件生命周期中持续存在,不会在每次重渲染时被销毁。
  • 可变引用:更新.current不会触发重渲染。
  • 常被用于执行命令式操作,或访问 DOM 中渲染出来的具体元素。

用 useRef 做 DOM 操作:聚焦按钮示例

一个典型场景:页面加载时自动聚焦某个按钮。

import { useRef, useEffect } from "react"; function ButtonComponent() { const buttonRef = useRef(null); useEffect(() => { buttonRef.current.focus(); }, []); return <button ref={buttonRef}>Click Me!</button>; }

实现原理分四步:

  1. react模块导入useRefuseEffect
  2. 调用useRef(null),返回一个包含current属性的对象,current的初始值就是传入的参数(这里为null)。与useState的初始值参数一样,该初始参数在后续渲染中会被忽略
  3. 创建useEffect,在组件挂载时执行一次,尝试调用按钮元素的focus方法;
  4. buttonRef挂到按钮元素的ref属性上,从而建立buttonRef与 DOM 中按钮的关联。

你可能会问:初始值是nullbuttonRef.current怎么会有focus方法?答案在于执行时机:React 总是先完成渲染和屏幕绘制,之后才执行useEffect。因此在 effect 执行时,ref 与按钮的连接已经建立好了。

关于依赖数组的细节,可回顾 how_to_deal_with_side_effects.md:useEffect默认在每次渲染后运行,传入空数组[]可让它仅在组件首次挂载时运行,而返回的清理函数则会在卸载或下次 effect 执行前被调用。

非破坏性 DOM 操作原则

useRef不只限于聚焦元素。滚动到指定位置、测量元素尺寸、触发动画……基本上所有你在原生 JS 里做过的 DOM 操作都可以通过 ref 完成。例如把上面useEffect改成:修改按钮文本,2 秒后再改回来。

useEffect(() => { buttonRef.current.focus(); buttonRef.current.textContent = "Hey, I'm different!"; let timeout = setTimeout(() => { buttonRef.current.textContent = "Click Me!"; }, 2000); return () => { clearTimeout(timeout); }; }, []);

有意思的是,这段代码永远不会触发组件重渲染

但这里有一条重要原则:上面这种直接改textContent的写法你不应该照做useRef只应用于非破坏性的 DOM 操作(聚焦、滚动、测量等),而改变文本这种本该由 React 声明式渲染负责的内容,交给 ref 直接修改会破坏 React 对 DOM 的所有权,属于"破坏性操作"。

为什么不用 querySelector

"为什么不直接用querySelector之类的原生方法?"——因为自己操作 DOM 违背了使用 React 的初衷。只要可能,我们应该让 React 自己向 DOM 提交变更。通过 ref 属性,我们把"元素连接"这件事交给了 React 管理,同时保留了在需要时直接访问元素的能力,这是声明式与命令式的合理折中。

useRef 与 useState 的对比

两者都能存储值,但存在本质区别:

特性useRefuseState
值类型可变引用(.current可直接赋值)不可变 state
更新是否触发重渲染不触发触发
适用场景无需渲染的信息、DOM 引用需要反映到 UI 的数据

useMemo:缓存昂贵的计算结果

先测量,再优化

在讨论优化技巧之前,必须强调一点:不要盲目优化。可以使用react模块提供的Profiler组件,或在 React 开发者工具中使用 Profiler 来测量渲染性能,用数据判断是否存在真实的性能瓶颈。很多情况下应用已经足够快,根本不需要优化。正如计算机科学领域的名言:

过早的优化是万恶之源。——《计算机程序设计艺术》Donald Knuth

问题场景:购物车总价每次渲染都重算

回顾之前的 Shopping Cart 项目:你需要计算加入购物车商品的总价。一个典型的Cart组件(抽屉式,用户每次点击"Add to Cart"或头部购物车图标都会打开)可能是这样:

function Cart({ products }) { const totalPrice = products.reduce( (total, product) => total + product.price * product.quantity, 0 ); return ( <div> {/* Some other content in the cart */} {/* Products to display */} <p> Total Price: <strong>${totalPrice}</strong> </p> {/* Some button to checkout */} </div> ); }

问题很明显:totalPrice在组件内部直接计算,每次组件渲染或更新都会从头算一遍reduce方法要遍历每个商品并做乘法和加法,当商品数量达到成百上千时,这个操作会越来越耗时。更糟的是,用户频繁打开/关闭购物车抽屉时,即使购物车内容没变,组件每次渲染都会做这些完全不必要的重复计算

用 useMemo 缓存计算结果

useMemo钩子为组件内部提供了记忆化能力:它缓存一次函数调用的结果,后续直接复用,只有当依赖发生变化时才重新计算。它的参数与useEffect相同:一个calculateValue回调和一个dependencies依赖数组。

import { useMemo } from "react"; function Cart({ products }) { const totalPrice = useMemo(() => { return products.reduce( (total, product) => total + product.price * product.quantity, 0 ); }, [products]); return ( <div> {/* Some other content in the cart */} {/* Products to display */} <p> Total Price: <strong>${totalPrice}</strong> </p> {/* Some button to checkout */} </div> ); }

useMemo的语法与useEffect几乎相同:挂载时运行回调;后续重渲染时,只有依赖(这里是productsprop)变化才重新运行回调,否则直接返回上一次缓存的值。这样一来,无论用户打开/关闭购物车多少次,只要products没变,totalPrice都直接命中缓存。

引用相等性(Referential Equality)检查

useMemo还有一项常被忽视的能力:稳定引用。来看一个用 Profiler 测量渲染性能的例子,这里还会引入memo

不需要真的启动 React 应用——先通读代码,思考会发生什么。这是一个很好的"读代码 + 可视化推演"练习。注意这只是一个非常基础的示例,真实项目中你会遇到大量向其他组件传递 props、且渲染开销很大的组件。

import { useState } from "react"; const ButtonComponent = ({ children, onClick }) => { let i = 0; let j = 0; const ITERATION_COUNT = 10_000; while (i < ITERATION_COUNT) { while (j < ITERATION_COUNT) { j += 1; } i += 1; j = 0; } return ( <button type="button" onClick={onClick}> {children} </button> ); }; function Counter() { const [count, setCount] = useState(0); const handleClick = () => { setCount((prevState) => prevState + 1); }; return ( <div> <h1>{count}</h1> <ButtonComponent onClick={handleClick}>Click me!</ButtonComponent> </div> ); }

ButtonComponent是一个独立组件(方便单独管理样式等),接收childrenonClick两个 props,内部用双重循环模拟高开销渲染。点击处理函数handleClick定义在Counter中,作为onClickprop 传给ButtonComponent

回顾重渲染心智模型:只要 state 或 props 变化,组件就会渲染,组件内不受 React 控制的函数、变量都会被销毁并重新执行。因此每次count变化,handleClick都会被重新创建,ButtonComponentonClickprop 也就跟着变了。

那么useMemo能帮上忙吗?既然它能缓存值,那就可以把函数引用缓存起来,用空依赖数组让它永不变化:

const memoizedHandleClick = useMemo(() => handleClick, []);

也可以直接这样写(useMemo可以缓存任何值,函数也只是值的一种):

// 第一个箭头函数是 useMemo 的回调; // 第二个箭头函数才是我们之后要调用的函数,它是被缓存的值,也是最终存入 handleClick 的内容 const handleClick = useMemo( () => () => setCount((prevState) => prevState + 1), [] );

但这样依然不够:因为每当父组件(Counter)的 state 变化,父组件会连带重渲染自己的子组件——换句话说,"父组件重渲染时,子组件也会重渲染自己"。所以即使onClick引用没变,ButtonComponent依然会重新渲染。

memo:跳过 props 未变化时的子组件渲染

React 提供了一个 API——memo包装函数,它可以在 props 未变化时跳过组件的重渲染(即使父组件重渲染了)。

import { useState, memo } from "react"; const ButtonComponent = memo(({ children, onClick }) => { let i = 0; let j = 0; const ITERATION_COUNT = 10_000; while (i < ITERATION_COUNT) { while (j < ITERATION_COUNT) { j += 1; } i += 1; j = 0; } return ( <button type="button" onClick={onClick}> {children} </button> ); });

memo包裹组件后,就阻止了从上方触发的那条"向下的更新传播"。这个组件现在只会在自己的 props 变化或自身 state 变化时重新渲染

把两者组合起来,会得到两种截然不同的结果(这正是引用相等性检查的体现):

  1. 传入的是普通handleClick+ButtonComponent包了memo仍然会重渲染。因为每次父组件渲染都会创建新函数,引用相等性检查失败(前一次的 prop不等于当前的 prop)。
  2. 传入的是memoizedHandleClick+ButtonComponent包了memo不会重渲染。因为函数引用被useMemo缓存住了,引用相等性检查通过(前一次的 prop等于当前的 prop)。

这个原理适用于所有作为 props 传递的值。它最常见的实际用途之一是与 Context API 配合,缓存 context 的value对象,避免每次渲染都生成新引用导致所有消费者重渲染(可结合 managing_state_with_context_api.md 中的 "Drawbacks of using Context API" 一节理解):

const value = useMemo( () => ({ someState, someFunction }), [someState, someFunction] ); return <Context value={value}>{children}</Context>;

useCallback:专门记忆化函数

useCallback提供了另一种记忆化方式,但它只能记忆化函数(不像useMemo能缓存任意值)。还记得前面用useMemo缓存函数引用的写法吗?

const handleClick = useMemo( () => () => setCount((prevState) => prevState + 1), [] ); // 或 const memoizedHandleClick = useMemo(() => handleClick, []);

useCallback就不需要这种嵌套写法了,它是专门为函数设计的:

import { useCallback } from "react"; // 组件内部 // 不使用 useCallback const handleClick = () => setCount((prevState) => prevState + 1); // 使用 useCallback const handleClick = useCallback( () => setCount((prevState) => prevState + 1), [] ); // 或 const memoizedHandleClick = useCallback(handleClick, []);

少了一层箭头函数,读起来更简洁。useCallback本质上没有超出"只记忆化函数"这一点的额外功能,所以useMemouseCallback的主要区别仅仅是返回值的类型

那么该用哪个?useMemo用于任意值类型,useCallback专门用于函数。归根结底两者做的事情几乎一样、只有微小差别,按你的偏好选择即可。

React Compiler:自动记忆化

除了手动优化,现在我们还有React Compiler——一个相对较新的构建期(build-time)工具,能够自动优化 React 应用。它会分析你的代码,自动为合适的组件和 hooks 添加记忆化,主要目标是提升更新性能(对已存在组件的重渲染)。这意味着在大多数情况下,我们不需要手动记忆化组件或 hooks

那么问题来了:既然 React Compiler 会自动记忆化,还需要了解React.memouseMemouseCallback吗?答案是:需要,原因有三:

  1. 理解手动记忆化,才能理解 React Compiler 在底层做了什么;
  2. 你几乎一定会遇到使用了手动记忆化技巧的现存代码库;
  3. 有些场景需要比 React Compiler 自动行为更精细的控制——例如确保某个useEffect的依赖被记忆化,使其引用不无故变化、从而不会不必要地触发 effect。

结论与实践建议

Refs 和记忆化都是不容易掌握的概念,但通过实践你会逐渐理解它们。要点总结:

  • useRef:在需要"记住信息但不触发渲染"、或需要直接访问 DOM 元素时非常有用;只做非破坏性 DOM 操作,让 React 负责提交 DOM。
  • useMemo / useCallback / memo:手动记忆化的三件套,用于优化昂贵计算与稳定引用;只在确实需要时才使用,不要过度优化。
  • React Compiler:无需写额外代码即可自动优化应用;但理解手动记忆化仍然必要。

这些主题也是常见的面试题,请务必弄清useMemouseCallback的区别。

动手实验

原课程提供了配套的可运行示例仓库:克隆后进入memoization/目录,执行npm install再运行npm run dev,然后动手修改代码、做各种实验,亲自验证上述"引用相等性检查通过/失败"两种场景。结合课程中 state 示例 的npm install && npm run dev工作流,你能更直观地感受优化前后的差异。

延伸学习路径

本课属于 React 进阶概念 模块,与以下课程形成完整闭环:

  • 不理解重渲染机制?回顾 introduction_to_state.md 与 rendering_techniques.md;
  • 想理清 effect 与依赖数组?阅读 how_to_deal_with_side_effects.md;
  • 想了解 context 值缓存与性能权衡?阅读 managing_state_with_context_api.md;
  • 想要用 reducer 整合复杂 state 逻辑?参考 reducing_state.md;
  • 购物车总价的例子来自 project_shopping_cart.md。

知识自测

  • 为什么应该优先使用useRef而不是querySelector之类的 DOM 操作方法?
  • useMemouseCallback的区别是什么?
  • useMemouseCallback是如何帮助优化 React 组件性能的?
  • 什么时候才应该记忆化一个值?
  • 什么是 React Compiler,它是如何优化 React 应用的?

【免费下载链接】curriculumThe open curriculum for learning web development项目地址: https://gitcode.com/GitHub_Trending/cu/curriculum

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 12:30:25

Typora内联公式详解:LaTeX语法、常见错误与高效技巧

Typora 算是我这几年写技术笔记、做实验记录、甚至攥毕业论文时最顺手的编辑器之一。它的 Markdown 即时渲染体验确实做得好&#xff0c;尤其是对 LaTeX 公式的支持&#xff0c;基本让我告别了“先在 Word 里用公式编辑器一个个点符号”的折磨。不过我也见过不少刚开始用 Typor…

作者头像 李华
网站建设 2026/9/15 12:29:48

Cesium+Vue地形开挖组件:基于ClippingPlanes的实时裁剪实现

简介&#xff1a;面向Cesium与VUE开发者&#xff0c;这是一套解决三维场景中地形开挖功能的完整组件方案&#xff0c;代码未加密、未压缩&#xff0c;可直接调用运行&#xff0c;帮助快速实现地形裁剪、局部开挖与效果展示。压缩包共6个文件&#xff0c;包含Vue组件、三个核心J…

作者头像 李华
网站建设 2026/9/15 12:28:11

AI范式转变:从技术演进到产业重构

1. AI范式转变&#xff1a;从技术演进到产业重构的深度解析当AlphaGo击败李世石时&#xff0c;我们以为看到了AI的巅峰&#xff1b;当ChatGPT写出学术论文时&#xff0c;我们才发现这只是开始。AI发展正在经历一场根本性的范式转变——这不仅关乎算法改进&#xff0c;更将彻底重…

作者头像 李华
网站建设 2026/9/15 12:26:56

Flutter流式JSON解析优化与鸿蒙适配实战

1. 项目背景与核心挑战在移动应用开发领域&#xff0c;处理大型JSON数据一直是个棘手的性能瓶颈。传统JSON解析方案如dart:convert库的json.decode()方法&#xff0c;会一次性将整个JSON文档加载到内存中。当处理10MB以上的JSON文件时&#xff0c;内存占用可能瞬间飙升到原数据…

作者头像 李华
网站建设 2026/9/15 12:26:27

Vue3封装SVG边框进度条:光点跟随动效与充电场景实现

前段时间在做一个车载HMI相关的展示项目&#xff0c;设计稿里给了一个很典型的边框进度条交互&#xff1a;卡片四周一圈发光描边&#xff0c;随电量增长而增长&#xff0c;同时有一个光点动画效果沿着边框游走&#xff0c;整体风格很像新能源车中控屏上的充电画面。当时第一反应…

作者头像 李华