红帽白皮书新鲜出炉!点击获取,让你的云战略更胜一筹! 了解详情
写点什么

什么时候要在 React 组件中写 shouldComponentUpdate?

  • 2016-06-30
  • 本文字数:3099 字

    阅读完需:约 10 分钟

生命中一半的时间都用来写 JavaScript 的 James K Nelson 最近发表了一篇文章,标题是《 Should I use shouldComponentUpdate? 》。他在这篇文章中介绍了应该在什么情况下使用 React 组件中的shouldComponentUpdate方法。

接触过 React 的人应该都知道它是一个非常快的前端框架,或许也听说过shouldComponentUpdate可以让它更快。但你知不知道它们在什么情况下才能发挥作用?也就是说,你知道什么时候需要动手写shouldComponentUpdate方法吗?

James 指出,如果你在 React 组件中写了shouldComponentUpdate方法后不能获得可测量的,并且是可察觉到的性能提升,那就不要写。

你的意思是我不应该用它?

按照 React 团队的说法,shouldComponentUpdate是保证性能的紧急出口,既然是紧急出口,那就意味着我们轻易用不到它。但既然有这样一个紧急出口,那说明有时候它还是很有必要的。所以我们要搞清楚到底什么时候才需要使用这个紧急出口。

为了讲清楚这个问题,James 对 React 的渲染机制做了深入地剖析。

他首先指出:

添加shouldComponentUpdate方法一般都会拖慢组件的更新速度。

为什么会这样呢?因为在他看来,React 基本上就是一个非常聪明的shouldComponentUpdate实现。它不仅知道应该在什么时候更新组件,还知道应该如何更新组件,并且这两件事情它都做得很好。那么 React 是如何知道是否应该更新组件的呢?这要从组件中的render方法说起。

尽管在写代码时,我们看到render返回的都是 JSX 或者ReactElement,但实际上,它返回的都是下面这种普通的 JavaScript 对象:

复制代码
{
type: 'ul',
props: { className: 'what-do-you-want-to-do-tonight' },
children: [{
type: 'li',
children: 'The same thing we do every night, pinky.'
},]
}

React 就是用这种对象来描述要在界面中渲染的标签。如果跟上次渲染时所用的对象比较,数据没有发生变化,显然就不用更新界面中的 DOM。

换句话说,React 已经替我们实现了一个shouldComponentUpdate。为了简化,我们可以假装props不是绑在组件的this上的,而是直接传给了render,那么 React 的实现基本上就是下面这样的:

复制代码
shouldComponentUpdate(nextProps) {
return !deepEquals(render(this.props), render(nextProps))
}

你是知道的,对于比较小的对象来说,deepEquals很快,但如果是个层层嵌套的大家伙,它的速度就不行了。因此我们可以得出第一条结论:

如果render的返回值很小,但props是个大家伙,那自己写shouldComponentUpdate很可能不会带来什么好结果。

那这是不是说,如果render返回的值足够大,我们自己写shouldComponentUpdate就会比较划算呢?实际上也不尽然。

讲到这里,James 又给出了他观察到的第二个事实:

使用shouldComponentUpdate得到的收益一般是微乎其微的。

他举了一个例子:比如要渲染一个table,我们从props中得到数据,然后又对这些数据做了些计算。并且这些数据都是放在 Immutable.js 的结构中的,因此通过比较引用是否相等就能判断出props是否发生了变化。

在这样一个场景中,如果我们自己写shouldComponentUpdate,那速度要比 React 默认实现的处理速度快很多。James 说他观察到的结果是最少快十倍!对,你没看错,他确实是这样说的,但他紧接着又说:

不足一毫秒的渲染时间在速度提升了 10 倍之后,依然也还是不足一毫秒。

嗯,我也觉得他这是在耍我们。

James 还搬出大神高德纳的那句名言来警告我们不要掉进过早优化的陷阱。为了引起足够的重视,他又指出了使用 shouldComponentUpdate所引发的问题:

shouldComponentUpdate很难维护

React 团队说shouldComponentUpdate是个紧急出口,而不是加速按钮应该就是出于这个原因。但 James 给出了一个更形象的比喻,他说用shouldComponentUpdate就是没有采取安全措施的性行为。

因为他觉得有时候很有必要写shouldComponentUpdate,并且那些时候shouldComponentUpdate肯定会让你的 app 有更好的表现。但是这也是导致 bug 的主要原因之一,并且还都是一些不太容易察觉的 bug。接着他又给出了几个具体的例子,并指出这些 bug 在测试中很难发现。如果在给客户演示的时候跳出来,那后果就不堪设想了。

什么时候需要写 shouldComponentUpdate?

因此还是回到了最初的那个问题上,什么时候需要自己动手写shouldComponentUpdate方法?他再次重申了前文中给出的那个答案:

只有经过测量,发现有了shouldComponentUpdate后组件的渲染速度确实有可察觉的提升,你才应该用它。

James 还在 fiddle 中给了一个例子,供我们练习如何测量,并比较使用 shouldComponentUpdate前后的渲染速度。

在开始测量之前先搞清楚如何在你的浏览器中打开分析器(如果你还不知道怎么做,Chrome 请看这里,Firefox 看这里)。

测量的过程很简单:

第一步:测量标准版

先从点击一个动作时render所花的时间开始测量:

  1. 在 fiddle 例子的窗口中打开 JavaScript 分析器。
  2. 开始记录。
  3. 点击"Toggle synergy!",让页面循环 5 秒钟左右。
  4. 点击开始记录时点过的那个按钮,停止记录。
  5. 按“self”时间排序,找到耗时最多的那个render方法,应该在列表的顶端附近。把它用的时长记下来,这里我们要的是“总时长”,即render本身所用的时间及它调用的函数所用的时间。

如果在这个列表顶部没找到render,那么恭喜你!你完全没必要写shouldComponentUpdate,最起码在解决掉其他性能问题之前没必要。

第二步:测量有shouldComponentUpdate的版本

这一步是要测量给App组件添加了shouldComponentUpdate之后渲染所花的时间。下面有个提前准备好的实现,你可以把它加到组件中:

复制代码
shouldComponentUpdate(nextProps, nextState) {
return !Immutable.is(this.state.synergy, nextState.synergy)
},

加好之后,按步骤一中的过程测量一遍。得到测量结果后,还要找到shouldComponentUpdate用的时间,然后加上去。应该差不多像下面这样:

组件中有shouldComponentUpdaterender

shouldComponentUpdate

就是这样,测过之后你就能 _ 大概 _ 知道shouldComponentUpdate能带来什么样的好处了。注意这里说的是 _ 大概 _,因为这种方法得不到精确的结果。一定要记住,真实结果跟测量结果比可能会有很大的差异,不信你可以多试几次。

做出决定

既然测量结果不精确,那我们凭什么做出决定呢?要凭好得不容置疑的测量结果。

那什么才算是好得不容置疑的结果呢?按照 James 的经验,如果加上shouldComponentUpdate之后渲染时间减少了一半,那用shouldComponentUpdate应该是真的对你有好处的。但同时也不要忘记,只有原来的渲染时间足够长时,这种性能上的提升才是有意义的。假如本来只用了 100ms,那你折腾半天加快的那点速度人们依然是感觉不到的。

所以在最终要做决定的时候,你要记住使用shouldComponentUpdate会带来维护上的挑战;并且测量结果是不准确的;而且性能改善的幅度还要是能感觉到的那种,只有记住这三点,你才能做出正确的决定。当然,这一切的前提是你的shouldComponentUpdate实现是没有问题的。

写出有效的 shouldComponentUpdate

在经过不懈地努力找到应该使用shouldComponentUpdate的点后,接下来的问题就是应该怎么写呢?

James 给出的答案非常:“Immutable.js!”。看到这个答案你的感觉是不是像看到下面这幅画一样?

当然,Immutable.js 不是唯一的答案,用Object.assign也可以,关键是不可变的状态。

如果你觉得 Immutable.js 也解决不了你的问题,James 又提出了一个更高级的解决方案:结构良好的状态。不过如果你想知道怎么才能做出结构良好的状态,只能听他下回分解了。


感谢韩婷对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们。

2016-06-30 18:0023360
用户头像

发布了 45 篇内容, 共 24.3 次阅读, 收获喜欢 10 次。

关注

评论

发布
暂无评论
发现更多内容

Termius for Mac(SSH客户端)v8.4.0激活版

iMac小白

SecureCRT for mac(终端SSH工具)v9.3.2激活版

iMac小白

为大模型工程提效,基于阿里云 ACK 的云原生 AI 工程化实践

阿里巴巴云原生

阿里云 AI 云原生

可观测实践丨如何利用 AI 算法解决告警配置三大难题?

阿里巴巴云原生

阿里云 云原生 可观测

服务器如何配置支持history模式

百度搜索:蓝易云

云计算 Linux 运维 云服务器 history

Topaz Video AI for mac(人工智能视频增强软件)v4.1.0激活版

iMac小白

当 OpenTelemetry 遇上阿里云 Prometheus

阿里巴巴云原生

阿里云 云原生 可观测

Seata 2.x 首个版本正式发布,支持 Raft 集群模式

阿里巴巴云原生

阿里云 云原生 seata

2023年的技术总结和工作反思

攻城狮Wayne

年终总结

MSE Nacos:解决敏感配置的安全隐患

阿里巴巴云原生

阿里云 微服务 云原生 nacos

活动回顾丨阿里云云原生 Serverless 技术实践营西安站 PPT 下载

阿里巴巴云原生

阿里云 Serverless 云原生

Macs Fan Control Pro for mac v1.5.16中文激活版下载

iMac小白

MATLAB R2023b for Mac v23.2.0.2428915中文激活版

iMac小白

从内核的视角观测容器——SysOM 容器监控

阿里巴巴云原生

阿里云 云原生 容器服务

2023启示录丨我的大模型创业这一年

自象限

创业 #大模型

随想2024.01.21

hackstoic

精力管理

文心一言 VS 讯飞星火 VS chatgpt (183)-- 算法导论13.4 7题

福大大架构师每日一题

福大大架构师每日一题

调研 7 个开源项目后,这家数据合规平台如何构建高性能网关

阿里巴巴云原生

阿里云 云原生

改变命运的抉择

少油少糖八分饱

小说 命运 东野圭吾 推理小说 悬疑

ubuntu系统如何查看已安装cudnn版本

百度搜索:蓝易云

Linux ubuntu 运维 云服务器 cudnn

阿里云 ACK One Serverless Argo 助力深势科技构建高效任务平台

阿里巴巴云原生

阿里云 云原生

Navicat Premium 15 for Mac(数据库开发工具)v15.0.36中文激活版

iMac小白

Microsoft Remote Desktop for Mac v10.9.5中文正式版下载

iMac小白

恭喜 Apache Dubbo 和 Nacos 荣获开放原子“2023年度生态开源项目”

阿里巴巴云原生

阿里云 开源 云原生

工作中常用到的java8相关操作总结

智慧源点

stream java8 LocalDateTime Optional

淘宝/天猫获取卖出的商品订单列表 API(taobao.seller_order_list)

技术冰糖葫芦

API

CentOS 8上使用NVM安装特定版本的Node.js教程

百度搜索:蓝易云

Linux centos 运维 Node 云服务器

FlagData 2.0:全面、高效的大模型训练数据治理工具集

mr.well

NLP 大模型 LLM模型 #大模型

Lightroom Classic 2024 for Mac(LRC2024)v13.0.2中文激活版

iMac小白

阿里云 ACK 云原生 AI 套件中的分布式弹性训练实践

阿里巴巴云原生

阿里云 分布式 云原生

Nacos 在云原生架构下的演进

阿里巴巴云原生

阿里云 云原生 nacos

什么时候要在React组件中写shouldComponentUpdate?_语言 & 开发_吴海星_InfoQ精选文章