资讯动态

结合 Refine 示例项目详解 React.memo:消除 React 组件的无谓重渲染

发布时间:2026/9/10 9:13:03 来源:尧图企业网站定制
结合 Refine 示例项目详解 React.memo消除 React 组件的无谓重渲染【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine本文以 Refine 仓库自带的博客演示项目为核心完整讲解 React 记忆化Memoization的原理与实战从理解记忆化解决的问题到用React.memo()包裹函数组件、编写自定义比较器再到用 Chrome DevTools 验证渲染行为。读完之后你将能够准确判断何时该用记忆化、如何用 props 比较控制重渲染时机并在真实组件树中定位不必要的渲染开销。什么是记忆化Memoization记忆化是一种性能优化技术核心思想是将函数最近一次计算的结果缓存下来当下次以相同参数再次调用该函数时直接返回缓存值跳过昂贵的重复计算。这样可以在时间与内存之间取得更好的平衡尤其适用于被反复调用、参数又经常重复的资源密集型函数。在 React 语境下函数可以是一次组件渲染也可以是一个数据处理工具函数因此记忆化有三个对应的工具React.memo()记忆化函数组件及其 props阻止因祖先组件重渲染引发的无谓渲染useMemo()缓存昂贵工具函数的计算结果避免重复的数据处理useCallback()记忆化传给子组件的回调函数避免每次渲染都生成新的函数对象。三者共同构成 React 性能优化的记忆化体系。本文聚焦第一个——React.memo()。为什么 React 需要记忆化React 通过先重渲染虚拟 DOM 组件、再对比更新真实 DOM 来工作但默认的重渲染传播机制会带来三类开销祖先重渲染引发的过度渲染任何祖先组件的重渲染默认都会触发其后代的级联重渲染。例如父组件的一次本地 state 更新会导致其所有子组件跟着重渲染。如果子组件根本不依赖那个变化的 state这些渲染就是纯粹浪费内存和 CPU 时间的无效渲染。昂贵的工具函数排序、过滤、映射等数据处理函数需要遍历大型数据集在渲染函数体内直接调用它们意味着组件每次重渲染都要重新执行一遍。向子组件传递回调父组件每次渲染都会重新创建内联回调函数对象导致子组件收到的 prop 引用不断变化、资源被无谓消耗并可能破坏基于引用比较的渲染优化。正确使用记忆化可以缓解以上问题但反过来说滥用记忆化本身也有代价——比较函数会引入额外计算有时不记忆化反而更快。因此用在哪比怎么用更关键。演示项目总览Refine 仓库中内置了配套的演示项目 examples/blog-react-memoization-memo这是一个由 Create React AppCRACO搭建的博客小应用模拟了一个展示最新博文与我的文章列表的页面。项目的组件代码已经全部写好本文通过逐个检查组件、观察浏览器控制台输出来验证记忆化的效果而不是从零搭建。项目结构从源码结构看演示应用的核心组件树如下src/App.jsx入口组件持有signedIn状态src/components/Blog.jsx主体页面持有updatedPosts与本地时间状态src/components/LatestPost.jsx最新博文卡片内部每 3 秒自增点赞数src/components/Post.jsx被React.memo记忆化的目标组件src/components/UserPostsIndex.jsx、UserPostsList.jsx、UserPost.jsx用户文章列表部分src/utils/sortPosts.js排序工具函数后续useMemo一节的对象src/fetch/fetchUpdatedPosts.js模拟拉取最新博文的函数。本地运行方式由于验证过程依赖浏览器控制台建议在本地跑起来再阅读。根据 package.json 中的脚本与依赖声明React 19、Node20运行步骤为# 1. 进入仓库中的演示项目目录 cd examples/blog-react-memoization-memo # 2. 安装依赖 yarn install # 3. 启动开发服务器对应 package.json 中的 start: craco start yarn start然后打开 Chrome 访问http://localhost:3000在 Ubuntu 上用Ctrl Shift J、在 Mac 上用Command Option J打开控制台观察渲染日志。提示仓库中该示例的 README.md 也记录了同样的启动流程craco.config.js中针对 ESM 模块的fullySpecified: false配置是为兼容 React 19 生态下的模块解析做的调整。组件逐个拆解App /组件App /是Blog /的宿主它用useState()保存signedIn状态并提供一个切换函数import { useState } from react; import Blog from ./components/Blog; function App() { const [signedIn, setSignedIn] useState(false); const handleClick () setSignedIn(!signedIn); console.log(Rendering App component); return ( main nav button onClick{handleClick}Sign Out/button /nav Blog signedIn{signedIn} setSignedIn{setSignedIn} / /main ); } export default App;对照 App.jsx 源码可以看到真实文件中Sign Out按钮通过onClick{handleClick}触发setSignedIn(!signedIn)而signedIn经由Blog /继续向下透传——这条 props 链是后文理解记忆化比较机制的基础。Blog /组件Blog /负责在点击Get Latest Post按钮时拉取博文列表并更新updatedPosts状态import React, { useEffect, useMemo, useState } from react; import fetchUpdatedPosts from ../fetch/fetchUpdatedPosts; import allPosts from ./../data/allPosts.json; import sortPosts from ../utils/sortPosts; import LatestPost from ./LatestPost; import UserPostsIndex from ./UserPostsIndex; const Blog ({ signedIn }) { const [updatedPosts, setUpdatedPosts] useState(allPosts); const [localTime, setLocalTime] useState(new Date().toLocaleTimeString()); const getLatestPosts () { const posts fetchUpdatedPosts(); setUpdatedPosts(posts); }; const sortedPosts sortPosts(updatedPosts); useEffect(() { const id setInterval( () setLocalTime(new Date().toLocaleTimeString()), 1000, ); return () clearInterval(id); }, []); console.log(Rendering Blog component); return ( div div{localTime}/div button onClick{getLatestPosts}Get Latest Post/button LatestPost signedIn{signedIn} post{sortedPosts[0]} / UserPostsIndex signedIn{signedIn} / /div ); }; export default Blog;注意两点updatedPosts每次渲染都会经过sortPosts工具函数排序这里存在重复计算正是后续useMemo()优化点localTime通过setInterval每秒更新一次意味着Blog /自身每秒都会重渲染一次。仓库中 Blog.jsx 还保留了被注释掉的useMemo写法供读者切换对比其导出语句目前是export default React.memo(Blog);见 Blog.jsx 第 54 行。LatestPost /组件LatestPost /嵌套了本文的主角Post /import React, { useEffect, useState } from react; import Post from ./Post; const LatestPost ({ signedIn, post }) { const [likesCount, setLikesCount] useState(null); useEffect(() { const id setInterval(() { setLikesCount((likesCount) likesCount 1); }, 3000); return () clearInterval(id); }, []); console.log(Rendering LatestPost component); return ( div {post ? ( Post signedIn{signedIn} post{post} / {likesCount ( div classNamemy-1 p-1 span{likesCount} Likes/span /div )} / ) : ( pClick on Get Latest Post button/p )} /div ); }; export default LatestPost;likesCount每 3 秒自增一次因此LatestPost /每 3 秒重渲染一次——而Post /作为它的子组件会被动地跟着重渲染。Post /组件Post /接收signedIn与post两个 props渲染博文内容import React from react; const Post ({ signedIn, post }) { console.log(Rendering Post component); return ( div className {post ( div classNamepost p-1 h1 classNameheading-sm py-1{post.title}/h1 p{post.body}/p /div )} /div ); }; export default Post;注意其中console.log(Rendering Post component)这行日志它是我们的探针。控制台观察无谓重渲染的复现启动应用后观察控制台会发现Post /的渲染日志跟着LatestPost /以 3000ms 的间隔不断输出——即使Post /的 propssignedIn、post完全没有变化它也会因为父组件likesCount的变化被反复重渲染。这些渲染对Post /来说毫无意义对应用性能却是实实在在的成本。下面用组件记忆化消除它们。用 React.memo() 记忆化函数组件对Post /的导出语句做一处修改const Post ({ signedIn, post }) { console.log(Rendering Post component); return ( ... ); }; export default React.memo(Post);在仓库的 Post.jsx 第 19–25 行这段记忆化代码以及自定义比较器都以注释形式保留着// const customComparator (prevProps, nextProps) { // return nextProps.post prevProps.post; // }; export default Post; // export default React.memo(Post); // export default React.memo(Post, customComparator);你可以通过注释/取消注释切换三种状态直接在控制台对比渲染日志。启用React.memo(Post)后Post不再随 3 秒一次的likesCount变化而重渲染日志频率明显下降。在一个真实社交应用中点赞这类高频状态更新引发的级联渲染是常见性能杀手这一步的优化收益相当可观。React.memo 到底是什么React.memo()是一个高阶组件HOC它接收一个函数组件返回一个记忆化后的新组件。这个新组件只在其 props 或内部 state 发生变化时才重渲染从而屏蔽掉它不关心的祖先 state 变化。从源码结构看其工作机制可以概括为React.memo(Post)记住了组件函数本身与上一次的 props 值当父组件再次渲染并传入新 props 时React 对上一次 props 与本次 props 做浅比较shallow comparison全部相等则复用上次渲染结果、跳过重渲染否则用新 props 重渲染并更新缓存。props 缓存与该渲染时才渲染Post /依赖signedIn和post这两个 props这与likesCount的情况不同。signedIn源自App /经Blog /、LatestPost /逐层透传下来顶部导航栏的Sign Out按钮负责切换它nav classNamenavbar button classNamebtn btn-danger onClick{handleClick} Sign Out /button /nav在Post /中加一行console.log(signedIn);后点击Sign Out按钮可以在控制台看到Post /在LatestPost /之后重新渲染了一次并打印出signedIn的新布尔值。这正符合预期——React.memo缓存了传入的 props 并检查变化signedIn变化刷新了记忆化结果触发一次有意义的重渲染。这正是记忆化想要的行为不因无关的祖先变化而渲染因自身依赖的 props 变化而渲染。React.memo() 最佳实践用于频繁重渲染的组件当组件因父组件重渲染而被反复渲染、且自身渲染开销较大时值得记忆化// 记忆化前父组件每次渲染都会带动 Post 重渲染 const Post ({ title, content }) { console.log(Rendering Post component); return ( div h1{title}/h1 p{content}/p /div ); }; // 记忆化后仅当 title 或 content 变化时才重渲染 const Post React.memo(({ title, content }) { console.log(Rendering Post component); return ( div h1{title}/h1 p{content}/p /div ); }); export default Post;避免滥用 React.memo()如果组件的 props 本来就每次都在变浅比较大概率总是失败记忆化反而增加了比较 缓存的额外开销收益为负。只对 props 不常变化、且因祖先更新而被拖累渲染的组件使用它。默认只做浅比较React.memo()默认对每个 prop 做浅比较。若传入的是复杂对象或数组可编写自定义比较函数只比较关心的字段const Post React.memo( ({ title, content }) { console.log(Rendering Post component); return ( div h1{title}/h1 p{content}/p /div ); }, (prevProps, nextProps) { return prevProps.title nextProps.title; // 只比较 title }, );上例中组件只在title变化时重渲染即使content变了也不会触发。不要记忆化静态或极少更新的组件静态组件不接收变化的 props本身就没有被祖先拖累的问题记忆化只会增加复杂度而非性能const Footer () { return footerFooter content/footer; }; // 这里没有必要使用 React.memo什么时候该用、什么时候不该用 React.memo该用的场景希望组件只响应经常变化或由事件驱动的特定 props屏蔽其余祖先 state 变化。例如我们的Post /只关心post与signedIn不关心LatestPost /的likesCount。不该用的场景如果 props 值本来就很少变化仅仅为了缓存某个值而记忆化等于白引入一个比较函数——比较本身有成本却省不下任何渲染。此时不记忆化才是更优解。结论是用测量的数据说话。借助 Chrome DevTools 等运行时工具先确认渲染热点在哪里再决定是否记忆化。自定义比较器只跟踪你要的 propsReact.memo()的第二个参数可以传入一个自定义比较函数import React from react; const Post ({ signedIn, post }) { console.log(Rendering Post component); return ( ... ); }; const customComparator (prevProps, nextProps) { return nextProps.post prevProps.post; }; export default React.memo(Post, customComparator);这里刻意把signedIn排除在比较之外只比较post。此后点击Sign Out按钮Post /不再重渲染——因为signedIn的变化已经不会被纳入比较只有post引用变化才会触发渲染。从源码结构看比较函数的返回值语义需要注意返回true表示 props 相等不重渲染返回false才会重渲染这与普通判断是否需要更新的直觉正好相反编写时容易弄反。用 DevTools 剖析组件渲染定位无谓渲染最可靠的工具是浏览器里的 React Profiler安装Chrome 或 Firefox 的 React Developer Tools 扩展按F12或Ctrl Shift I打开 DevTools切到Profiler标签点击Record开始录制在录制期间与应用交互点击Get Latest Post、Sign Out等按钮Profiler 会记录每个组件的渲染次数与耗时停止录制后在时间线中查看每个组件的Render Time单次渲染耗时与Render Reason触发重渲染的 props/state 变化对确认无谓重渲染的组件如Post用React.memo()包裹验证改进效果在Components面板开启Highlight Updates组件每次重渲染时页面会闪烁高亮可以直观看见哪些区域在无效更新。在本文的演示应用中开启 Highlight Updates 后再对比注释/启用React.memo(Post)两种状态Post区域是否每 3 秒闪烁一次差异一目了然。进阶记忆化中的深比较与浅比较浅比较默认行为React.memo()默认使用浅比较只比较对象/数组的顶层引用不会深入嵌套属性。两个内容相同但引用不同的对象会被判定为不相等const person1 { name: John }; const person2 { name: John }; console.log(person1 person2); // false —— 引用不同对原始类型字符串、数字、布尔的 props浅比较行为符合直觉但当 props 是对象或数组时只要父组件渲染时重新生成了该对象哪怕内容完全一样引用就变了记忆化组件就会照常重渲染优化失效。深比较深比较会逐层检查所有嵌套属性成本明显更高。React 默认不做深比较正是因为面对深层嵌套对象或大数组时它可能比省下的渲染更慢。用自定义比较器实现深比较当 props 是复杂对象、且你希望内容相同就不重渲染时可以用第三方库的深比较函数作为比较器import React from react; import { isEqual } from lodash; const Post ({ post }) { console.log(Rendering Post component); return ( div h1{post.title}/h1 p{post.content}/p /div ); }; const customComparator (prevProps, nextProps) { return isEqual(prevProps.post, nextProps.post); // lodash 深比较 }; export default React.memo(Post, customComparator);这里用lodash的isEqual()对整个post对象做深比较即使父组件每次渲染都新建了post对象只要内容没变Post /就不会重渲染。也可以用deep-equal等库达到同样目的。深比较的适用场景与原则复杂数据结构props 是大对象或深层嵌套对象且你明确不希望引用变化引发重渲染权衡性能深比较本身有成本务必测量确认省下的渲染确实大于比较的开销实践原则优先浅比较仅在确定 props 是深层嵌套且内容稳定时才上深比较。更常见的工程做法是在父组件侧用useMemo()稳定引用从源头保证浅比较能够命中。小结本文围绕 Refine 仓库中的演示项目 examples/blog-react-memoization-memo完整走通了React.memo()的实践路径原理React 默认会因祖先重渲染级联重渲染后代React.memo()作为 HOC 通过 props 浅比较拦截无谓渲染验证借助console.log探针与 3 秒一次的likesCount间隔在浏览器控制台中观察到Post /从每 3 秒渲染一次变为仅 props 变化时渲染边界自定义比较器如 Post.jsx 中保留的注释代码让你精确选择要跟踪哪些 props方法用 Chrome DevTools 的 Profiler 与 Highlight Updates 先测量、后优化避免滥用记忆化。React.memo()解决的是组件层面的记忆化对于演示项目Blog /中每次渲染都会执行的 sortPosts 这类昂贵计算以及传给子组件的回调函数则分别对应useMemo()与useCallback()的优化场景——它们是记忆化体系中与React.memo()互补的另外两块拼图。【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价