将 React 应用优化到 60fps翻译自React at 60fps,从属于Web 前端入门与工程实践。

作为 DOM 的抽象,React 自然也遵循了著名的抽象漏洞定理(详见2016-我的前端之路:工具化与工程化),引入 React 导致了在应用本身的性能消耗之外势必会增加额外的性能损耗。Dan Abramov 在 Twitter 上提到,React 并不能保证性能优于原生的 DOM 实现,但是它能够帮助大量的普通开发者构建大型应用的同时不必在初期就耗费大量的精力在性能优化上,在大部分用户交互界面上 React 已经能够帮我们进行合理的优化了。但是在应用开发的过程,特别是最后的细节优化阶段中,我们需要着眼于部分性能瓶颈页面,正确地认识这种限制的缘由以及相对应的处理方案。本文即是作者在构建自己的大型应用中经验的总结。

避免过早优化

无论你在做的是啥应用,注意要避免如惊弓之鸟般过早优化。换言之,在你真实的发现某些性能问题之前不要为了优化而优化,在 React 中,如果我们进行过多的冗余优化拆分操作反而会造成奇怪的 Bug。正常的性能优化过程应该包含以下几个步骤:

  • 确定发现存在性能的缺陷

  • 使用 DevTools 来解析发现瓶颈所在

  • 尝试使用优化技巧解决这些问题

  • 测试是否确实有性能提升

  • 重复第二步

React 15.4 中引入了新的性能评测工具,可以方便地与 Chrome DevTools 集成使用,从而大大简化我们性能定位地困难。

需要使用 shouldComponentUpdate 吗?

相信几乎每个 React 开发者都会熟悉组件生命周期中的 shouldComponentUpdate,React 会根据该函数返回的布尔值来判断是否需要重渲染该组件。在我最初用 React 的那段时间,我天真的以为 React 会智能地帮我们在 Props 与 State 没有改变的时候取消组件重渲染,不过事实证明只要你调用了setState或者传入了不同的 Props 的时候,React 就会重渲染组件。而重载这个shouldComponentUpdate方法可能是最简单的组件优化方式了,不过这种方式仍然存在某些不足或者副作用。譬如当你在某个高阶组件中重载了该方法之后,尽管你只是希望不重渲染该组件,而实际上 React 可能会依赖于该组件的shouldComponentUpdate返回值而取消对组件树中的整个子组件的重渲染。基于该函数最著名的实现当属shouldPureComponentUpdate,该重载方式会浅层比较当前的与未来的 State 以及 Props 的差异,这种方式的缺陷如下:

  • 它并没有深层比较两个对象,不过如果它真的进行了深层比较,该操作会变得异常缓慢,这也就是使用不可变数据结构的原因。

  • 如果传入的某个 Props 是某个回调函数,那么该函数会一直返回True

  • 比较检测本身也是有性能损耗的,应用中过多的冗余比较反而会降低性能。

总结而言,在实际应用开发中,建议的重载shouldComponentUpdate应该适用于以下类型的组件:

  • 使用简单 Props 的纯组件

  • 叶子组件或者在组件树中较深位置的组件

不过还是需要强调的是,无论你是选择重载shouldComponentUpdate函数还是使用pure HoC这样的模式,还是首先应该找出那个拖慢整个应用性能的组件。

将高性能消耗的代码放置到较高阶组件中

如果在你的渲染函数中存在着部分性能消耗较高的计算代码,那么建议是将这部分代码尽可能地放置到较高阶的组件中,或者使用memorizing(reselect)的方式来减少重复调用或者计算。在我构建https://status.postmarkapp.com/网站的过程中,我主要通过以下的方式优化整体性能:

  • 将可视化数据与覆盖层信息抽取出来放置到独立的组件中。

  • 将执行大量数据转换的代码移出到容器组件中。

  • 仅仅对于可视化组件与覆盖层组件覆写shouldComponentUpdate函数。

  • 使用不可变数据结构(Immutable)来降低比较带来的性能消耗

我发现最常见的降低应用性能的原因就是用户输入引发的 DOM 操作,譬如用户滚动或者鼠标移动的响应,都会大幅度的降低应用的帧数。这些事件往往都会以较高地频次触发,如果你打算监听并且响应任何用户细小的动作,那么估计你的应用离崩溃不远了。我们通常会使用debounce模式来避免频繁地触发响应,不过这种模式也会让用户觉得应用不是那么灵活响应,这里我们再讨论下应该以怎样的方式来解决这个性能问题。

同步滚动组件

为了更好地解释这个问题,我构建了某个同步滚动的组件来演示这个问题,其效果如下所示:

该组件的主要职能在于保持左右两个滚动面板的一致性(就好像常见的MarkDown预览),而因为两个面板的内容高度不一致,因此两个面板需要以不同的速度进行滚动。

不要滥用 this.setState

React 应用开发中最常见的某个错误就是对于this.setState函数的使用,我们不应该将render()函数中用不到的状态放置到this.state对象中。下面我们来看下第一个版本的滚动面板的实现:

class ScrollPane extends React.Component {componentDidUpdate() {// Each time we get new props we set the // new scrollTop position on the DOM elementthis.el.scrollTop = this.props.scrollTop}render() {<div ref={(el) => {this.el = el}}>}
}class ScrollContainer extends React.Component {constructor() {super()this.leftPane = nullthis.rightPane = nullthis.state = {leftPaneScrollTop: 0,rightPaneScrollTop: 0}}handleLeftScroll = (evt) => {// Calculate new scrollTop positions// for left and right panes based on// DOM nodes and evt.target.scrollTopconst leftPaneScrollTop = …const rightPaneScrollTop = …// Don't do this since this will re-render everything// on each `scroll` event!this.setState({leftPaneScrollTop,rightPaneScrollTop})}render() {return (<div><ScrollPane ref={(el) => {this.leftPane = el}} onScroll={this.handleScroll}scrollTop={this.state.leftPaneScrollTop}><ExpensiveComponent /></ScrollPane><ScrollPaneref={(el) => {this.rightPane = el}}onScroll={this.handleScroll}scrollTop={this.state.rightPaneScrollTop}><ExpensiveComponent /></ScrollPane></div>)}
}

在这个版本的实现中,我们将所有的状态放置到了this.state中,此时问题就在于每次你调用this.setState来设置组件状态时,React 会重渲染整个组件树。另外,我们是否真的有必要将scrollTop的值以 Props 的方式传递到子组件中?我们可以先将滚动高度从组件状态对象中提取出来:

handleScroll = (evt) => {// Calculate new scrollTop positions// for left and right panes based on// DOM nodes and evt.target.scrollTopthis.leftPaneScrollTop = …this.rightPaneScrollTop = …
}

将滚动高度作为类成员属性就不会触发重渲染,不过此时我们应该如何更新兄弟组件的滚动位置呢?这里的建议是直接进行 DOM 操作。虽然这种方式看起来有点破坏 React 声明式组件的特性,不过笔者在前文中也提到过,声明式的特性与 DOM 操作并不相冲突。我们可以使用 Context(虽然貌似这个也不建议使用)来操作子组件而避免直接操作子组件的命令式代码,从而保证其他组件仍然保持纯粹的声明式。代码如下:

export default class ScrollPane extends Component {static contextTypes = {registerPane: PropTypes.func.isRequired,unregisterPane: PropTypes.func.isRequired};componentDidMount() {this.context.registerPane(this.el)}componentWillUnmount() {this.context.unregisterPane(this.el)}render() {return (<div ref={(el) => { this.el = el }}>{this.props.children}</div>)}
}export default class ScrollContainer extends Component {static childContextTypes = {registerPane: PropTypes.func,unregisterPane: PropTypes.func}getChildContext() {return {registerPane: this.registerPane,unregisterPane: this.unregisterPane}}panes = []registerPane = (node) => {if (!this.findPane(node)) {this.addEvents(node)this.panes.push(node)}}unregisterPane = (node) => {if (this.findPane(node)) {this.removeEvents(node)this.panes.splice(this.panes.indexOf(node), 1)}}addEvents = (node) => {node.onscroll = this.handlePaneScroll.bind(this, node)}removeEvents = (node) => {node.onscroll = null}findPane = node => this.panes.find(pane => pane === node)handlePaneScroll = (node) => {window.requestAnimationFrame(() => {// Calculate new scrollTop positions// for left and right panes based on// DOM nodes and evt.target.scrollTop// and set it directly on DOM nodesthis.panes.forEach((pane) => {pane.scrollTop = …})})}render() {return (<div><ScrollPane><ExpensiveComponent /></ScrollPane><ScrollPane><ExpensiveComponent /></ScrollPane></div>)}
}

在上述实践中,ScrollContainer组件实现了register/unregister方法用来添加或者删除面板以及注册 DOM 监听事件。而ScrollPane组件仅用来在挂载时注册,在卸载时注销。每次面板触发onScroll事件的时候,回调函数会获得新的滚动高度然后自动为其他面板设置scrollTop位置值。可以在这里查看源代码,并且这种方式也用于了 React Native 的 Animated。

将 React 应用优化到 60fps相关推荐

  1. react性能优化方案_React灵敏且性能卓越的Spray + Akka解决方案,以“在Java和Node.js中发挥并发性和性能”...

    react性能优化方案 在我以前的文章中,我研究了一个虚拟的交易引擎,并将基于Java的阻止解决方案与基于Node.js的非阻止解决方案进行了比较. 在文章的结尾,我写道: 我怀疑随着Node.js的 ...

  2. [react] 你知道的react性能优化有哪些方法?

    [react] 你知道的react性能优化有哪些方法? shouldComponentUpdate PureComponent :Class Component React.Memo :Functio ...

  3. React性能优化记录(不定期更新)

    React性能优化记录(不定期更新) 1. 使用PureComponent代替Component 在新建组件的时候需要继承Component会用到以下代码 import React,{Componen ...

  4. React性能优化总结

    文章同步于Github Pines-Cheng/blog 初学者对React可能满怀期待,觉得React可能完爆其它一切框架,甚至不切实际地认为React可能连原生的渲染都能完爆--对框架的狂热确实会 ...

  5. React性能优化SCU | PureComponent | memo

    文章目录 React性能优化SCU React更新机制 render函数被调用 PureComponent 高阶组件memo React性能优化SCU React更新机制 我们在前面文章已经讲解过Re ...

  6. React 性能优化完全指南,将自己这几年的心血总结成这篇!

    作者: MoonBall 原文地址: https://juejin.cn/post/6935584878071119885 本文分为三部分,首先介绍 React 的工作流,让读者对 React 组件更 ...

  7. 声音甜美的美女工程师已就位-帮你搞定React面试的疑难杂症 React面试优化教程

    声音甜美的美女工程师已就位-帮你搞定React面试的疑难杂症 React面试优化教程 React早已经是一线大厂前端的必备技术了,那么在前端跳槽过程中能够帮助同学们的加分的就是在面试这个环节了.Rea ...

  8. Airbnb 爱彼迎房源详情页中的 React 性能优化

    Airbnb 爱彼迎工程师和数据科学家将定期和大家分享移动开发.系统架构.数据科学及人工智能等领域的技术探索和经验心得. 正文从这开始-- 在一些容易被忽视但又非常重要的场景,可能会有许多严重影响性能 ...

  9. React性能优化(完整版)

    我的博客 http://wangxince.site/my-demo-markdown/ React 性能优化 1.减少 render 次数 shouldComponentUpdate PureCom ...

最新文章

  1. abaqus切削为什么没有切屑_SiCp/Al复合材料超声振动辅助切削研究现状与进展
  2. (十八)深入浅出TCPIP之HTTP和HTTPS
  3. 最有用的Postgres扩展:pg_stat_statements
  4. Axure快速原型教程02--创建页面和设置界面
  5. android java框架_【阿里P8大牛教你Android入门之路(java篇)】——Java集合框架(系列篇1)...
  6. video 标签内 音量_HTML5 视频(Video)元素使用详解
  7. mysql双一参数_MySQL 的双1设置-数据安全的关键参数(案例分享)
  8. 如何利用wordpress搭建自己独立的博客(个人网站)
  9. 计算机ip地址和用户名和密码忘记了怎么办,192.168.1.112这是IP地址 我把密码忘了 怎么处理啊 电脑室现在还...
  10. Vim插件合集 (打造你的专属炫酷IDE)
  11. 【论文笔记】Don’t Stop Pretraining: Adapt Language Models to Domains and Tasks
  12. 如何生成密钥文件Snk
  13. 2016山东教师教育网- 一师一优课
  14. ajax 滚动加载 缓存,Ajax实现加载缓存的loding效果
  15. 如何在线批量转换图片格式为jpg?
  16. 阿里P8总结的Nacos入门笔记,从安装到进阶小白也能轻松学会
  17. ctfshow 89-115 php特性 wp
  18. 一荣俱荣,豪取多项冠军后荣耀着手年终奖改革
  19. 在apache中开启deflate方法的总结
  20. pip install mpi4py报错:ERROR: Could not build wheels for mpi4py, which is required to install pyprojec

热门文章

  1. 元数据驱动应用设计_设计驱动的应用内购买:创造可持续的获利能力
  2. 解决 nginx 启动错误 nginx [emerg] host not found in upstream
  3. epub怎么转txt?快来看看这篇文章
  4. 【shell】压缩包解压密码暴力破解流程
  5. Linux服务器的关机和重启命令
  6. MegaUpload + MegaRotic Mega Manager 1.0.1.5
  7. PS--把美女头像转成漂亮的仿手绘效果
  8. python生成excel模板_实例9:用Python自动生成Excel档每日出货清单
  9. 落月星辰:做自己的孤勇者,也做鸡蛋的终结者
  10. 增长黑客 - 开源项目增长利器