如何提高你的 React 应用的性能

如何提高你的 React 应用的性能

(How to greatly improve your React app performance)- Noam Elboim / from medium

本文旨在总结常见的性能缺陷,以及如何来避免这些缺陷。

性能问题在web应用开发中不是什么新鲜事。

我们每个人都有这样的时刻,当你把一个新的Component组件放到你的app中,你会突然发现你尝试的每一个用户交互动作都与期望的效果有很明显的滞后。有时,你可以重复使用多个同样的组件,这种尴尬的动效滞后会更加明显。像下面这样:


在那一刻你也许心里已经给写这个组件的人起了好几个绰号了。但是最好的办法是:做些什么,是的,你可以的!

我们将重点解决以下几个常见的 React 性能问题:

1.错误的 shouldComponentUpdate 实现 ,为什么 PureComponent 没能拯救你。

2.太快的改变 DOM。

3.滥用事件(events)和 回调(callbacks)。

对于以上的每个问题,我们先解释问题的根源,然后我们提出一些简单易用的方法来避免它。

管好你的 shouldComponentUpdate

组件的component 钩子函数 shouldComponentUpdate 的本意是用来阻止一些非必需的渲染(render), shouldComponentUpdate将即将更新的props和state作为参数,如果返回值是true, render函数就执行,否则不执行 render.

React.Component 默认的实现 shouldComponentUpdate 是返回true.

越多的render渲染意味着耗费越多的时间。所以我们需要防止不必要的更新来减少额外的时间。

为此,你会想到我们应该在实现 shouldComponentUpdate 的时候更谨慎些。

问题

让我们看一个简单的使用 shouldComponentUpdate 的例子:


Simple shallow implementation: 'this.props.children !== nextProps.children', but it's always returning true

code

等下,为什么不起作用呢?

不起作用是因为 React 每次渲染的时候创建了一个新的 ReactElement!

这就意味着 在 shouldComponentUpdate函数中 Shallow Comparision 如:return this.props.children !== nextProps.children;几乎就相当于return true;

根据我的经验,大多数组件通常都以某种方式支持 ReactElement props(PropTypes.node or PropTyps.elemtn)比如像children这是很常见的情况。

那么, PureComonent又是怎样的呢?

React.PureComponent 是React.Component 的另外一种方式。它不是总在其 shouldComponent 实现中返回true,而是 props 和 state 的浅层比较。

使用 PureComponent 会返回同样的结果,如下:


PureComponent component is still always returning true


这是 PureComponent 特性的bug吗?我不确定。我们需要知道的是,PureComponent 在大多数情况下不起作用,它并不能阻止一些不必要的更新。

可能的解决方案

我们第一点想到的是——进行深度比较! 这确实管用,但是它有两个重要缺陷:

1. 运行深度比较本身是一个过程比较长,比较重,比较耗时的动作。因此,在 shouldComponent 函数运行结束之后,render 函数才能运行。这样一来性能非但不能提升反而会变得更差。

2. 这只是基于当前的 React Elements 实现,在未来版本中可能会取消。

综上,在我看来,使用深度比较并不是一个好的解决办法。

为了寻找到更好的解决方案,我研究了一些其他的虚拟 DOM 库,看看他们是怎么解决这个问题的。

我发现了 Vue 作者Evan You 一个关于在Vue.js中添加 类React shouldeComponent 的 feature request 发表的一个有意思的评论。他解释到,这个问题并不能通过 "diffing" 虚拟DOM解决,因为它有很多未知的问题。依赖 React Elements 来检测组件中的状态变化并不是一个可行的解决方案。

在实际应用中,不应该在 shouldComponentUpdate 的实现中使用 React Elements 的比较作为返回结果。相反,应该使用某种状态的改变来告诉组件是否应该更新。

我们应该基于prop的不同来通知 state 的改变,而不是通过使用this.props.children !== nextProps.children。最好是一个数字或者字符串,这样比较会更快。

我们甚至可以使用一个新的 prop 专门用来通知组件是否应该更新。

更进一步,我和我的同事创建了一个高阶组件(HOC)。这个组件使用继承反转(Inheritance Inversion)来扩展通用的 shouldComponent 实现,也是 PureComponent 的替代方案。 而且确实有效。代码在这里:

https://github.com/NoamELB/shouldComponentUpdate-Children

必须说明的是,这只是一个通用的实现,所以并不是适用所有的情况。具体可以参考这里

例子在这里,使用了一个自定义的 shouldComponentUpdate 实现。正如上面提到的,它确实不会再进行不必要的渲染了。


几种比较:


具体示例代码可以参考这里

允许你的组件扩张

你是否在你的应用中多次使用相同的组件,致使你的应用非常重动画也很卡顿,有时候即使使用一个也会导致应用性能的损耗?

问题

在创建复杂的组件时,你可能需要执行一些自定义 DOM 的操作。在创建的时候你可能会遇到两个问题:

1. 触发太多布局(Layout)而没有使用触发复合(Composite)或者重绘(Paint)

2. 太多没必要的Layout.多次读写DOM,导致 DOM不必要的重新计算。

让我们看下 原生 Collapse 组件,在0和内容高度之间改变它的高度。点击查看


当使用一个这样的组件时,可以正常展示。但是当你多次使用的时候......


点击查看具体效果

如果你不是在移动设备上查看,可能感觉不明显。需要将你的chrome performance选项调到 6x slowdown


可能的解决方案

让我们分析下 Collapse 组件发生了什么——这是高度改变的时候的代码:


这里有两个问题需要注意:

1. 我们改变的height属性,根据csstriggers.com这个列表,改变高度(height)触发了布局(Layout)的重新计算。如果我们设法改变类似transform的东西,那只会触发Composite,并且会更平滑些,对吗?

事实正式如此,这样会表现更好,但是这样就会在Collapse组件下留下一个空白,因为我们没有改变它的高度。

2. 上面代码的第三行,这是常见的改变高度出发Layout的滥用:我们从DOM读取了高度this.contentEl.scrollHeight然后又通过this.containerEl.style.height对DOM设置了高度,然后多次重复这样的操作。

如果我们可以成组的一次性读取过来高度,然后再一次性设置高度,这样不是更好吗?

批量的读写 DOM 是一个很好的减少 Layout 的尝试。我们可以使用requestAnimationFrame对DOM 读写进行批量处理,像下面这样:


requestAnimationFrame能保证你的代码在浏览器下一帧触发,减少页面绘制成本,按需批量绘制。让你的动画更流畅。点击查看具体实现

这样用起来可能比较麻烦,那么可以使用内置组件或者使用第三方库比如Fastdom, Fastdom也是基于requesAnimationFrame 的原理通过批量处理DOM 读取/写入 操作来消除频繁的Layout操作。

值得一提的是,由于浏览器和设备功能的限制,有时您可能无法获得足够好的性能。在这些情况下,最好的解决方案可能是变更产品需求。

最后,你可能听过css的will-change属性。在特定的情况下它可以帮助你,但是使用不好也会有一定的风险。最好不要过度使用它。

管住你的 callbacks

当我们调用任何 DOM 事件的时候,有一个去抖(debounce)或者节流(throttle)函数时很有必要的。它可以让我们把这个函数的调用次数减少到我们想要的最低限度,以此来提高性能。

通常像这样写:window.addEventListener(‘resize’, _.throttle(callback)),但是为什么我们不能把它也运用到 React Components callbacks 里呢?

问题

让我们看下面这个组件:


有没有注意到,我们每次输入改变都会调用this.props.onChange, 它会被调用多次,虽然很多调用都是非必需的。如果父级正在根据onChange回调进行 DOM 更改或者任何其他比较繁重的操作,我们的应用会变得很卡顿。

可能的解决办法

其实我们可以这样改进:


Debounce the event

现在,只有在用户输入完成后才调用props.onChange, 这样就阻止了很多不必要的事件操作。

另外相似的解决办法还有函数节流(throtle).点击查看throttledebounce区别

总结

这些工具应该可以帮助您处理一些我们在React应用程序中遇到的性能问题。通过明智地使用shouldComponentUpdate,控制你对DOM做的改变,并通过debounce / throttle来延迟回调,你可以大大地提高你的应用程序的性能。

如果你想测试开发遇到的情况,请查看UiZoo。它是React组件的一个动态组件库,它可以解析你的组件并展示给你,让你可以开发,测试或与他人共享。

完整示例代码见:示例代码



最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,710评论 4 376
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,839评论 2 308
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,295评论 0 255
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,776评论 0 223
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,198评论 3 297
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,074评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,200评论 2 322
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,986评论 0 214
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,733评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,877评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,348评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,675评论 3 265
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,393评论 3 246
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,209评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,996评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,212评论 2 287
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 36,003评论 2 280