相信不少React开发者都有过这样的经历父组件里一个很普通的状态更新比如拖动一下侧边栏、改一次筛选条件、刷新一下某个计数屏幕上的整个列表、每个卡片、每行数据全都跟着重新渲染了一遍。数据量小的时候还能忍一旦列表几百上千行、每行又是一个嵌套了好几层的重组件卡顿感立马就上来了。React性能优化里最常被拿出来讲的shouldComponentUpdate就是处理这类问题的第一把钥匙。这期内容从实际项目场景出发把shouldComponentUpdate的原理、写法、适用边界、常见坑位一口气理清楚。不管你是刚入门React、准备跳槽刷面试题还是正在为某个页面优化焦头烂额看完应该都会有自己的思路。1. shouldComponentUpdate到底在解决什么问题先看React的渲染流程1.1 为什么React会选择“无脑”重渲染很多新手对React渲染有一个误解觉得React足够聪明props没变的话组件就不会重新渲染。实际上默认情况下完全不是这样。React的re-render触发条件主要有几个组件自身调用了setState、收到的props引用发生了变化、父组件重新渲染、context值发生变化。其中最容易被忽略的就是“父组件重新渲染”这一条——只要父组件render了它生成的所有子组件React元素都会被重新创建无论子组件接收的props看起来有没有变。这是React刻意做出的设计取舍。React核心团队的原则是“宁可多渲染也不要漏渲染”因为一旦漏掉某个更新界面残留的就是脏数据这种bug远比性能问题难查得多。所以React把“props是否真的发生了值级别的变化”这个判断交给开发者自己决定而不是框架默认帮你做。shouldComponentUpdate就是类组件里手动控制这个决策的入口。可以拿装修打个比方默认情况相当于装修队进场后不问青红皂白先把整套房子的墙都铲了重刷而shouldComponentUpdate就像工头在每户门口问一句“这间屋子到底要不要动工”。问的人越多判断开销越大但能省下大把粉刷时间。1.2 SCU在组件生命周期中的位置类组件生命周期里shouldComponentUpdate的执行时机非常明确它在props或state发生变化、即将触发render之前被调用。首次挂载时不会调用它因为首次挂载无论如何都必须渲染。调用的时候React会传入两个参数nextProps和nextState分别代表接下来要应用的新props和新state。在方法内部通过this.props和this.state拿到的还是当前旧值。返回值决定后续行为返回trueReact继续走render流程返回falseReact跳过当前组件的render并且连带跳过它整个后代子树的渲染。注意这个连带机制很关键后面讲性能收益时全指望它了。但有一点要特别强调SCU返回false不能阻止子组件自身通过setState触发的更新它只管“由父级变化导致的渲染”。1.3 SCU不是银弹而是一笔交易一个经常被忽略的事实是SCU判断本身是有成本的。每次render之前都要额外执行一次方法调用方法内部如果做了复杂的比较逻辑这个成本可能比省下来的渲染成本还高。所以SCU的精髓并不在于“给每个组件都加上”而在于判断成本明显低于渲染成本时把它加在渲染代价较大的组件上。比如一个纯展示型组件props里就一个字符串渲染出来就是一行文字那渲染成本很低加了SCU反而多了一次函数调用。而一个列表项组件内部有图片加载、有多个子组件嵌套、有复杂的样式计算渲染一次可能要3到5毫秒这时候用几微秒做一次浅比较就是非常划算的买卖。整篇内容后面所有的实操和策略都是在围绕这笔“交易”怎么做得划算。2. 如何正确写shouldComponentUpdate从基础语法到三类常见策略2.1 基础写法比较目标字段而不是偷懒全量比较最笨的写法是把所有props都用循环比较一遍但实际工程里我几乎不这么干。更常见的是针对业务字段做精准比较。假设你有一个列表项组件OrderItemprops里包含order对象里面有id、status、price、items和一个onAdd回调那么SCU通常写成这样class OrderItem extends React.Component { shouldComponentUpdate(nextProps) { return ( nextProps.order.id ! this.props.order.id || nextProps.order.status ! this.props.order.status || nextProps.order.price ! this.props.order.price ); } render() { // 渲染图片、价格、状态标签等 return div classNameorder-item{/* ... */}/div; } }这里有个细节容易踩坑列表项的key对应的id字段如果id没变但order内部其他字段变了而你的SCU里只比较了id就会漏掉这次更新界面上出现脏数据。反过来也一样如果你把items这样的复杂数组整个纳入了比较又容易因为父组件生成了新数组引用而误触发渲染。所以字段选择本质上是“把真正影响渲染结果的字段挑出来”要覆盖全又不能过度覆盖。在SCU里使用JSON.stringify做全量比较是我见过最典型的反面教材shouldComponentUpdate(nextProps) { return JSON.stringify(this.props) ! JSON.stringify(nextProps); }这个写法的问题很明显序列化两个对象的时间往往比渲染组件本身还贵而且props里字段顺序一旦变了就会误判为不同无条件触发渲染。除非你的组件渲染成本高到离谱否则千万别这么干。2.2 PureComponent帮你完成浅比较的现成方案React早就想到了大多数人写SCU就是比较props和state所以直接提供了一个内置实现React.PureComponent。它继承自Component然后在shouldComponentUpdate里自动做了props和state的浅比较。浅比较的意思是对每个key调用Object.is进行比较只比较第一层引用。如果你给PureComponent传入的props里有一个对象对象内部属性变了但引用没变PureComponent会认为props没有变化class OrderItem extends React.PureComponent { render() { // ... } }这个例子中如果父组件拿到的order对象是同一个引用仅改了order.status字段PureComponent的浅比较会直接跳过这次渲染UI就不更新了。这正是后面要讲到的“浅比较陷阱”。所以使用PureComponent有一个前提所有props和state都要遵循不可变数据模式每次修改都生成新的对象或数组引用。2.3 函数组件里的React.memoSCU的现代继任者React 16.6之后推出的React.memo是PureComponent在函数组件世界里的等价物。它的用法很简单const OrderItem React.memo(function OrderItem(props) { // 渲染逻辑 });React.memo同样默认做浅比较。它还可以接收第二个参数传入自定义比较函数这是它比PureComponent更灵活的地方const OrderItem React.memo( function OrderItem(props) { // 渲染逻辑 }, (prevProps, nextProps) { // 返回 true 表示 props 相等不需要重新渲染 // 返回 false 表示 props 不相等需要重新渲染 return prevProps.order.id nextProps.order.id; } );这里有个非常容易记反的点SCU返回true表示“需要渲染”而React.memo的第二个参数返回true表示“不需要渲染”。两者的语义正好相反。我刚接触React.memo时在这个地方栽过跟头写反之后发现页面完全不更新排查了半天才反应过来。2.4 三种方案怎么选优化方案适用组件类型默认比较逻辑自定义能力shouldComponentUpdate类组件无完全由开发者实现最强可任意比较PureComponent类组件props和state浅比较弱没有自定义入口React.memo函数组件props浅比较中第二参数可传比较函数个人经验类组件项目里如果组件比较简单且props结构扁平直接用PureComponent最省事如果比较逻辑复杂老老实实写SCU函数组件一律用React.memo需要纠偏时传自定义比较函数。三种方案的底层思想完全一致本质上都是“在render之前拦截一次”。3. 实测用shouldComponentUpdate给商品列表砍掉80%多余渲染3.1 搭一个“渲染成本很高”的列表场景光讲原理不过瘾直接看一个完整案例。假设你在做一个订单列表页OrderList组件维护筛选状态、排序状态和购物车状态。列表里每一项都是一个OrderItem组件内部要渲染商品图片、价格、物流状态、营销标签还嵌套了一个PriceTag子组件整体属于典型的重组件。伪代码如下function OrderList() { const [filter, setFilter] useState(all); const [cart, setCart] useState([]); const orders getOrders(filter); return ( div FilterBar onChange{setFilter} / {orders.map((order) ( OrderItem key{order.id} order{order} / ))} /div ); }现在问题来了用户每点一次FilterBar上的按钮setFilter会触发OrderList整个函数重新执行紧接着getOrders重新生成一份订单数组所有OrderItem的props都会被重新赋值然后每个OrderItem都走一遍render流程。哪怕这次筛选操作只影响了其中两三个订单的展示状态剩余几十个订单也会全部跟着重渲染一遍。我在调试这个问题时习惯先在render里挂一个计数器直观看到渲染次数window.__renderLog window.__renderLog || {}; class OrderItem extends React.Component { render() { const key this.props.order.id; window.__renderLog[key] (window.__renderLog[key] || 0) 1; console.log(OrderItem ${key} 渲染次数: ${window.__renderLog[key]}); // 实际渲染逻辑 } }打开控制台点击一次筛选按钮会刷出一整屏的日志几乎每个订单项都被渲染了一遍。这个现象如果出现在生产环境的移动端页面里卡顿几乎是一定的。3.2 给OrderItem接上SCU精准跳过无关订单接下来对OrderItem做改造。这个组件只关心order.id、order.status、order.price这三个字段是否真正变化其他字段变化时完全不需要重新渲染。因此SCU写成这样class OrderItem extends React.Component { shouldComponentUpdate(nextProps) { return ( nextProps.order.id ! this.props.order.id || nextProps.order.status ! this.props.order.status || nextProps.order.price ! this.props.order.price ); } render() { // 原渲染逻辑保持不变 } }这一步做完之后再点一次筛选按钮你会发现渲染日志明显变少只有那些status或price真正发生变化的订单项还在渲染其他订单项直接跳过了整个render流程。如果这个项目用的是函数组件也可以用React.memo实现同样效果const OrderItem React.memo( function OrderItem({ order }) { // 渲染逻辑 }, (prevProps, nextProps) { return ( prevProps.order.id nextProps.order.id prevProps.order.status nextProps.order.status prevProps.order.price nextProps.order.price ); } );注意这里的返回语义返回true代表“相等不需要渲染”和SCU的返回语义正好相反。3.3 函数props的引用稳定问题useCallback不能省如果OrderItem还需要接收一个点击加购的回调函数比如onAddToCart情况会变得微妙。假设父组件这样写{orders.map((order) ( OrderItem key{order.id} order{order} onAddToCart{(id) setCart((list) list.concat(id))} / ))}这里每次OrderList重新render都会创建一个全新的箭头函数即使React.memo的自定义比较函数忽略了onAddToCart浅比较模式的默认memo也会因为函数引用变化而认为props不同导致SCU/memo的拦截失效。所以在函数组件里必须配合useCallback稳定函数引用const handleAddToCart useCallback((id) { setCart((list) list.concat(id)); }, []);这个细节是很多“我明明加了memo为什么没用”的终极原因面试里也经常拿这个当陷阱题。3.4 用React DevTools Profiler量化收益代码改完最好还是用数据说话。打开React DevTools的Profiler面板点击一次筛选操作会看到这次交互涉及的组件渲染耗时分布。优化前OrderList里的几十个OrderItem全部被点亮单次交互的总渲染耗时可能就在十几毫秒甚至更高优化后只有少数几个组件点亮总耗时可以压到一两毫秒以内。如果页面有Perfomance面板也可以直接用浏览器录制一段筛选操作的性能profile对比优化前后的Scripting和Rendering耗时。我实操下来列表项越多、每个项渲染越重SCU带来的收益越明显。几十个轻量组件的场景收益不大但几百上千个中重组件就非常值得做。4. 踩过的坑与排查技巧SCU不是“加了就完事”4.1 坑1数据是可变的PureComponent直接“失灵”这是使用PureComponent或SCU后最常见的bug没有之一。场景一般是这样的state里有一个对象代码里写push、直接赋值修改属性而不是生成新对象handleUpdate () { const newItem this.state.item; newItem.name new name; this.setState({ item: newItem }); };这种写法下引用没有变化PureComponent的浅比较一看新旧props引用相同直接跳过渲染。于是界面上永远是旧数据。排查方法很简单在render里打印this.props复制一份到控制台对比旧值会发现对象引用完全一致。解决办法只有一条路改成不可变更新handleUpdate () { this.setState((prev) ({ item: { ...prev.item, name: new name }, })); };展开运算符生成全新对象引用变了浅比较才能感知到变化。这也是为什么每次聊SCU都要把不可变数据放在一起讲两者是配套关系。4.2 坑2浅比较挡不住深层次变化浅比较只比较第一层引用。如果props里有一个嵌套对象比如cart数组里的商品数量变了但数组引用没变PureComponent或默认memo都会认为没有变化。这种情况下比较粒度已经超出了浅比较的能力范围。我不太推荐为了处理这种情况去写深比较成本高、容易误判。更健康的思路是拆分组件把深层数据变化影响的小区域抽成独立组件让它的props尽可能扁平和简单。比如把价格展示抽成PriceTag组件价格变化只影响它一个组件父级不参与比较这样既绕开了深嵌套比较又提高了更新触发的精准度。4.3 坑3SCU返回false反而破坏了组件自身状态SCU的return false影响面比很多人想象的大它不只是拦住“来自父组件的渲染”也会拦住“自身state变化引起的渲染”。如果一个组件内部有展开、选中等本地状态在SCU里又只比较了props字段就可能出现用户点开弹窗没反应、展开某个区域瞬间被收起这种诡异问题。避免方式SCU的比较逻辑里务必把this.state和nextState也纳入考量。更省心的做法是把内部状态相关的组件拆出来主组件只负责纯展示这样SCU就能专注比较props不用操心state。4.4 坑4不要在SCU里做随机、异步、时间相关的操作SCU是一个被React高频调用的方法每次渲染前都可能执行。如果在这个方法里读取Math.random()或者new Date().getTime()会导致渲染结果不可预测有时跳过渲染有时不跳过产生难以复现的闪烁问题。SCU必须是纯粹的函数同样的props和state输入必须产生相同的判断结果不依赖任何外部可变状态。另外不要在SCU里发起异步请求。这个方法不是做副作用的地方它没有任何生命周期语义保证React完全可能在某些情况下调用它而不继续render。4.5 坑5盲目给所有组件加SCU反而拖累性能SCU判断本身有开销如果给一个渲染成本极低的组件强行套上比较逻辑省下来的渲染时间可能还不及判断消耗的时间。更重要的是每个自定义SCU都是一份业务逻辑维护成本不低稍有不慎就引入一个不更新的bug。我的原则是“先度量再优化”先在Profiler里看哪些组件的渲染耗时排在最前面再针对热点组件做SCU/memo。不要一上来就给一个系统里所有组件加memo那只是自欺欺人的“看起来很努力”。5. 面试与实战SCU、PureComponent、React.memo的性能优化组合拳5.1 面试高频题React性能优化有哪些手段SCU处于什么位置这类问题最好分层回答让面试官看到你有完整的知识框架。通常我会按数据层、组件层、引用层、渲染调度层来展开。数据层包括不可变数据、合理拆分state、避免大范围context更新组件层就是今天聊的SCU、PureComponent、React.memo引用层是useCallback和useMemo负责稳定传给子组件的函数和对象渲染调度层则对应useTransition、useDeferredValue这类并发特性用于处理高频输入和重型更新的场景。SCU在这套体系里的定位非常清晰它是开发者第一次有机会告诉React“这个组件不用渲染”。其他方案都是在源头上减少渲染次数SCU是在渲染前设置一道闸门。5.2 面试高频题为什么父组件re-render子组件默认也会re-render这条背后的原因是React默认不做props值的深比较它只关心“React元素是否被重新创建”。父组件render后所有子组件的React元素都会被重新创建哪怕props值一模一样。React为了保证更新不被漏掉选择了“全部重新走一遍”的保守策略把精准判断交给开发者。这个设计在架构上有它的合理性React不需要追踪每个props字段是否变化也就不需要做大规模脏检查。代价就是渲染次数偏多需要SCU/memo来做增量式兜底。5.3 面试高频题SCU、PureComponent、React.memo的区别回答要点可以归结为三条。第一SCU是类组件完全自定义的控制方法你写什么React就执行什么第二PureComponent是内置了浅比较的Component子类省去自己写SCU的麻烦第三React.memo是函数组件版的PureComponent但支持传入第二个参数做自定义比较。还需要额外说明React.memo比较函数返回true表示“相等不需要渲染”和SCU的语义相反。这类题面试官真正想考察的是你是不是只背了API名字还是能把每个方案的边界说清楚。能讲出浅比较的局限性和不可变数据配合的必要性基本就能过关了。5.4 面试高频题为什么加了React.memo页面还是很卡最常见的答案集中在引用稳定性上。React.memo默认做浅比较如果父组件每次渲染都重新生成对象字面量或函数memo就会认为props变了拦截完全失效。比如这样的代码OrderItem info{{ name: foo }} /每次父组件render都会创建一个新对象memo浅比较发现引用不同无论如何都会渲染。解决办法要么保证对象引用稳定要么写自定义比较函数按字段判断。除了引用问题还得确认是否真的存在“多余渲染”。如果组件本身确实需要频繁更新那memo本来就不该起到拦截作用卡顿的根源可能另有他处需要继续往下挖比如图片加载、layout抖动、大数据渲染等等。5.5 实战决策表场景推荐做法类组件、渲染较重、条件简单PureComponent类组件、比较逻辑复杂自定义shouldComponentUpdate函数组件、props结构扁平稳定React.memo默认浅比较函数组件、需跳过特定字段变化React.memo自定义比较函数子组件接收函数propsuseCallback稳定函数引用深嵌套数据、高频局部更新拆组件细化状态粒度结尾一点个人体会我在实际项目中有一个雷打不动的习惯任何SCU或memo改造之前先在render里挂一个计数器或者用Profiler录一段操作改完之后再录一段前后对比数量级差异。没有数据支撑的优化很容易变成自我安慰。有一次生产环境出了个奇怪的同步问题列表里的价格怎么点都不更新排查了半天才发现同事在自定义比较函数里漏了price字段。这个案例让我后来一直坚持把SCU的比较函数当成业务逻辑来review而不是当成“加了就变快”的黑盒。shouldComponentUpdate确实是一把性能优化的关键钥匙但它需要理解React的渲染时机、配合不可变数据、并结合引用稳定性一起使用。想清楚“判断成本”和“渲染成本”这笔账你才能真正用好它。
企业数字化 ERP 产品动态
相关推荐
Agent应用实践之四十三 - OpenClaw:heartbeat心跳机制配置与验证 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 3:18:03
AI短剧工业化流水线:6步可落地的全流程生产方法论 1. 这不是“AI视频课”,而是一套可落地的短剧工业化流水线最近在B站刷到一个标题特别扎眼的教程:“【LibTV教程】目前B站最详细的一站式制作教程!从剧本、分镜、人物生成、视频、配音到剪辑完整演示,零基础手把手操作,… · 2026/9/26 3:18:03
NixOS 上部署 GNS3 Server:模块化配置、安全加固与源码级原理解析 包管理器操作系统 【免费下载链接】nixpkgs Nix Packages collection & NixOS 项目地址: https://gitcode.com/GitHub_Trending/ni/nixpkgs 点击查看 免费下载 GNS3(Graphical Network Simulator 3)是业界广泛使用的网络软件模拟器&… · 2026/9/26 3:57:26
OpenClaw大模型API怎么选?DeepSeek与Kimi配置实战:TaoToken统一Key接入 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 3:57:26
win11部署OpenClaw实测:Node.js、git、npm 环境一次跑通 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 3:57:26
【C/C++学习】inline 【C/C学习】inline 最近在学内核相关的书籍,其中一个关键字,对于它的理解不够深入,只知道它可以用于消除函数调用和返回带来的开销,但是发现它的存在不止于此 文章目录【C/C学习】inline整个总结inline是什么inline不一定保证函数… · 2026/9/26 3:57:26
Raven如何实现主动提醒还不打扰:Sentinel主动性引擎五层防线拆解 Raven如何实现主动提醒还不打扰:Sentinel主动性引擎五层防线拆解 【免费下载链接】Raven The Harness of Harnesses: a trusted, persistent, self-evolving multi-agent ecosystem for all-domain collaboration. 项目地址: https://gitcode.com/gh_mirrors/rave… · 2026/9/26 3:57:20
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍 简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践 一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46