将 React 应用优化到 60fps
作为 DOM 的抽象,React 自然也遵循了著名的抽象漏洞定理(详见2016-我的前端之路:工具化与工程化),引入 React 导致了在应用本身的性能消耗之外势必会增加额外的性能损耗。Dan Abramov 在 Twitter 上提到,React 并不能保证性能优于原生的 DOM 实现,但是它能够帮助大量的普通开发者构建大型应用的同时不必在初期就耗费大量的精力在性能优化上,在大部分用户交互界面上 React 已经能够帮我们进行合理的优化了。但是在应用开发的过程,特别是最后的细节优化阶段中,我们需要着眼于部分性能瓶颈页面,正确地认识这种限制的缘由以及相对应的处理方案。本文即是作者在构建自己的大型应用中经验的总结。 避免过早优化无论你在做的是啥应用,注意要避免如惊弓之鸟般过早优化。换言之,在你真实的发现某些性能问题之前不要为了优化而优化,在 React 中,如果我们进行过多的冗余优化拆分操作反而会造成奇怪的 Bug。正常的性能优化过程应该包含以下几个步骤:
React 15.4 中引入了新的性能评测工具,可以方便地与 Chrome DevTools 集成使用,从而大大简化我们性能定位地困难。
需要使用 shouldComponentUpdate 吗?相信几乎每个 React 开发者都会熟悉组件生命周期中的 shouldComponentUpdate,React 会根据该函数返回的布尔值来判断是否需要重渲染该组件。在我最初用 React 的那段时间,我天真的以为 React 会智能地帮我们在 Props 与 State 没有改变的时候取消组件重渲染,不过事实证明只要你调用了
总结而言,在实际应用开发中,建议的重载
不过还是需要强调的是,无论你是选择重载 将高性能消耗的代码放置到较高阶组件中如果在你的渲染函数中存在着部分性能消耗较高的计算代码,那么建议是将这部分代码尽可能地放置到较高阶的组件中,或者使用memorizing(reselect)的方式来减少重复调用或者计算。在我构建https://status.postmarkapp.com/网站的过程中,我主要通过以下的方式优化整体性能:
我发现最常见的降低应用性能的原因就是用户输入引发的 DOM 操作,譬如用户滚动或者鼠标移动的响应,都会大幅度的降低应用的帧数。这些事件往往都会以较高地频次触发,如果你打算监听并且响应任何用户细小的动作,那么估计你的应用离崩溃不远了。我们通常会使用debounce模式来避免频繁地触发响应,不过这种模式也会让用户觉得应用不是那么灵活响应,这里我们再讨论下应该以怎样的方式来解决这个性能问题。 同步滚动组件为了更好地解释这个问题,我构建了某个同步滚动的组件来演示这个问题,其效果如下所示: 不要滥用 this.setStateReact 应用开发中最常见的某个错误就是对于 class ScrollPane extends React.Component { componentDidUpdate() { // Each time we get new props we set the // new scrollTop position on the DOM element this.el.scrollTop = this.props.scrollTop } render() { <div ref={(el) => {this.el = el}}> } } class ScrollContainer extends React.Component { constructor() { super() this.leftPane = null this.rightPane = null this.state = { leftPaneScrollTop: 0,rightPaneScrollTop: 0 } } handleLeftScroll = (evt) => { // Calculate new scrollTop positions // for left and right panes based on // DOM nodes and evt.target.scrollTop const 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> <ScrollPane ref={(el) => {this.rightPane = el}} onScroll={this.handleScroll} scrollTop={this.state.rightPaneScrollTop} > <ExpensiveComponent /> </ScrollPane> </div> ) } } 在这个版本的实现中,我们将所有的状态放置到了 handleScroll = (evt) => { // Calculate new scrollTop positions // for left and right panes based on // DOM nodes and evt.target.scrollTop this.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 nodes this.panes.forEach((pane) => { pane.scrollTop = … }) }) } render() { return ( <div> <ScrollPane> <ExpensiveComponent /> </ScrollPane> <ScrollPane> <ExpensiveComponent /> </ScrollPane> </div> ) } } 在上述实践中, (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |