【译】React性能工程(下) -- 深入研究React性能调试

24 February 2016 on React

本文是 React 性能工程系列文章的 第二篇(共两篇). 在第一篇 译文,我们讲述了如何使用React性能工具和一些普遍存在的性能瓶颈,以及一些调试相关的技巧。如果你还没阅读上一篇文章,建议读一读!

本文我们将深入研究调试的工作流 -- 有了这些ideas之后,我们又要怎么实践呢?我们找了一些实际开发中遇到的例子,使用 Chrome 开发工具来诊断、修复这些性能问题。(如果你有好的建议或补充,欢迎让我们知悉!)

我们通过下面的示例代码来看下 -- 你将看到一个用React实现的简单版 todo list。点击下面 JS fiddle 中的 "RESULT" 查看交互效果、完成性能复制。我们将一步步更新 JS fiddle 来查看性能调试。

实例研究 #1: TodoList

从这个 TodeList 开始吧。快速地输入没有经过优化的代码,你会发现它运行缓慢。

我们打开 Chrome 开发者工具 Timeline profiler,它会展示浏览器的详细执行情况,包括执行用户事件、运行JS和渲染页面。在Input框输入一个字符,然后中止 timeline profiler。由于我们只是输入一个简单字符,所以这种迟缓并不明显,但它却是生成性能分析所需最小信息量的最快方式。

我们注意到 Event (textInput) 的长条,在脚本处理上总计耗时121.10毫秒。从 timeline profiler 可以看出,导致性能缓慢的是脚本问题,不是样式或计算引起的。

因此我们来看下脚本处理,切换到 Profiles 面板。Timeline 展示浏览器的概览并且支持JS Profile,而Profiles 则提供多种可视化工具,允许我们深入研究JS-land。以下是另一个 Profile 记录,表明性能的缓慢不是来源于我们的应用代码:

看下这个Profile,Total 这列根据占用时间递减排列,可以看出绝大部分时间是花在React的batchedUpdates的调用上,这点相当明确地暗示了是在React-land这一层。相反, Self 一栏评估了花费在函数本身的时间(排除耗费在子函数的时间),这样可以看出是否有一些特别耗时的函数。从这两个方面看来,用户层函数并没有明显的性能瓶颈。因此,我们换用React的性能工具来试下。

为了给这个缓慢的action生成一个测量概况,我们在控制台调用 React.addons.Perf.start(), 输入一个字符来执行这个action,随后调用 React.addons.Perf.stop() 完成这个流程。这样我们就可以看到React.addons.Perf.printWasted() 花费了一些不必要的时间:

第一列表明 TodoItem 是由 Todos 渲染出来的;然而,Perf.printWasted() 的打印结果表明:如果避免重新渲染,可以节省100毫秒。这个似乎是主要的优化项之一。

为了诊断为何 TodeItem 会浪费这么多时间,我们创建了一个自定义 mixin, 并把它命名为 WhyDidYouUpdateMixin。把它 hook 到组件中,哪部分代码更新及其更新的原因都打印出来。以下就是我们的代码,你可以根据自己所需,随意适配。

一旦我们把这个 mixin 放到 TodoItem 里面,我们可以看到这样的结果:

呀!我们看到 tagsbeforeafter 是一样的 -- mixin 告诉我们如果两个对象相等(不严格相等)是可以避免更新的。另一方面,计算出两个方法是否相等的过程也是很耗时的,因为 Function.bind 尽管带同样的参数,也会生成一个新函数。虽然这些都是有用的线索 -- 我们回头看下在 tagsdeleteItem 我们是怎么做的,似乎就是我们每传一个新的值,都创建了一个 TodoItem

如果我们通过一个未绑定的函数来传递给TodoItem,并用一个常量来储存tags,就可以避免这个问题了:

现在 WhyDidYouUpdateMixin 显示前一个props和新的props是浅相等的。我们可以使用 PureRenderMixin,如果前后两个props(和state)浅相等,则不用更新。

当我们再次运行 profiler,发现现在只是用了35毫秒(比之前快了4倍):

这样比起之前已经好很多了,但仍不够理想。Input 框的输入不应该这么耗时。因此,我们继续优化这个问题。刚刚仅仅是减少了常量,我们仍然需要对每个 item 做浅对比。

在这点上,你或许觉得一个 todo list上面有1000个 item 已经很特殊了,30毫秒对于你的应用来说是可以接受的。但是,如果你要支持上千个子item,这样就不符合理想中的60fps(每帧16毫秒)。

下一步比较合理的做法是把一个组件拆分成多个子组件 (这也可以说是有效的第一步)。我们注意到 Todos 组件实际上包括两个互不相交的子组件:一个AddTaskForm子组件包含了输入框和按钮,另一个 TodoItem 子组件包含items的列表。

每一步重构都能获得性能的提升:

  • 假设我们用 PureRenderMixin 创建一个TodoItems组件,它不用重新渲染每个item,就可省去部分优化工作,这时prevProps.items === this.props.items

  • 假设我们创建了一个 AddTaskForm 组件,文本输入后的状态就已经更新在那里了。当输入框文本变化时,Todos 组件就不用再重新渲染了。

这两步结合起来,每次按键只需要10毫秒!

实例研究 #2:

方案: 当用户的任务项太多( >3000)时,我们就渲染一个 warning,并且给这些 todo items 添加样式,这样其它每个item就都有一个背景颜色。

实践:

  • 我们用一个类似于 todo list 的例子,伴随着 TodoItems 的执行 -- 在这个例子中,我们把input框中的内容储存在组件状态的top-level

  • 我们创建一个 TaskWarning 组件,根据任务项的数量来渲染提示信息。要在组件内部封装这些逻辑,如果不用渲染,我们就让它返回null。

  • 我们给div:nth-child(even)添加灰色背景。

观察报告: 在Input框快速输入,页面变得有点迟缓(不超过3000个任务)。如果我们第一次给 todo list 再添加一项( > 3000 个任务),在按下按钮的那一瞬间,这种迟缓反而销声匿迹了。太令人惊讶了,添加更多的任务反而能够修复页面迟缓的问题!

调试: timeline profile 展示了一些非常有趣的报告:

基于某种原因,输入一个简单的字符会造成大量样式被重新计算,这个会耗时30毫秒(这也是为什么当我们输入的速度大于 30毫秒/字符时,可以观察到闪退的原因)。

查看 First invalidated 这一行,它表明 Danger.dangerouslyReplaceNodeWithMarkup 造成布局失效,需要重新计算样式。以下是 react-with-addons.js: 2301:

`oldChild.parentNode.replaceChild(newChild,oldChild);`

基于某些原因,React用一个全新的DOM节点来替换原来的DOM节点。重新调用那些DOM操作是很耗性能的!使用 Perf.printDOM() ,可以查看到React是怎样进行DOM操作的:

update attributes 表明在 input 框输入 abc 时,TaskWarning 还是不可见的。然而,replace 指出React正准备接触DOM来调用 TaskWarning 组件,尽管它看似有完全一致的虚拟DOM。

正如这里所表明的,React (<= v0.13) 使用一个 noscript 标签来渲染 no component, 但却不恰当地把这两个标签的功能处理得不一致:末尾的 noscript 标签是不需要用另一个noscript标签来代替的。 此外,之前我们给每个div添加了灰色背景。基于CSS,3000个item节点里面每个独立个体的渲染都取决于它的兄弟节点。每次 noscript 标签被替换,其后的DOM节点都会重新计算它们的样式。

为了解决这个问题,我们可以这样做:

  • TaskWarning 返回一个空的 div

  • TaskWarning 组件移到一个 div 里面,这样它就不会影响到其后节点的CSS选择器。

  • 升级React :-)

但这是脱离本意的。这里主要是我们知道怎么通过 timeline profiler 去诊断这些性能问题。

总结

希望这章能够帮助大家了解 React 的性能问题是如何在开发者工具呈现出来的 -- 把 TimelineProfiles 和React性能工具结合起来用大有帮助。

有上千个items的 todo lists 随意着色似乎是别扭的,但当渲染大量的文件和样式表,或者构建一个电子手册,我们都会遇到非常相似的问题。而且,我们仍然在壮大我们的团队 -- 如果你有兴趣构建复杂的React apps,欢迎联系我们

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


react 中的高阶组件主要是对于 hooks 之前的类组件来说的,如果组件之中有复用的代码,需要重新创建一个父类,父类中存储公共代码,返回子类,同时把公用属性...
我们上一节了解了组件的更新机制,但是只是停留在表层上,例如我们的 setState 函数式同步执行的,我们的事件处理直接绑定在了 dom 元素上,这些都跟 re...
我们上一节了解了 react 的虚拟 dom 的格式,如何把虚拟 dom 转为真实 dom 进行挂载。其实函数是组件和类组件也是在这个基础上包裹了一层,一个是调...
react 本身提供了克隆组件的方法,但是平时开发中可能很少使用,可能是不了解。我公司的项目就没有使用,但是在很多三方库中都有使用。本小节我们来学习下如果使用该...
mobx 是一个简单可扩展的状态管理库,中文官网链接。小编在接触 react 就一直使用 mobx 库,上手简单不复杂。
我们在平常的开发中不可避免的会有很多列表渲染逻辑,在 pc 端可以使用分页进行渲染数限制,在移动端可以使用下拉加载更多。但是对于大量的列表渲染,特别像有实时数据...
本小节开始前,我们先答复下一个同学的问题。上一小节发布后,有小伙伴后台来信问到:‘小编你只讲了类组件中怎么使用 ref,那在函数式组件中怎么使用呢?’。确实我们...
上一小节我们了解了固定高度的滚动列表实现,因为是固定高度所以容器总高度和每个元素的 size、offset 很容易得到,这种场景也适合我们常见的大部分场景,例如...
上一小节我们处理了 setState 的批量更新机制,但是我们有两个遗漏点,一个是源码中的 setState 可以传入函数,同时 setState 可以传入第二...
我们知道 react 进行页面渲染或者刷新的时候,会从根节点到子节点全部执行一遍,即使子组件中没有状态的改变,也会执行。这就造成了性能不必要的浪费。之前我们了解...
在平时工作中的某些场景下,你可能想在整个组件树中传递数据,但却不想手动地通过 props 属性在每一层传递属性,contextAPI 应用而生。
楼主最近入职新单位了,恰好新单位使用的技术栈是 react,因为之前一直进行的是 vue2/vue3 和小程序开发,对于这些技术栈实现机制也有一些了解,最少面试...
我们上一节了了解了函数式组件和类组件的处理方式,本质就是处理基于 babel 处理后的 type 类型,最后还是要处理虚拟 dom。本小节我们学习下组件的更新机...
前面几节我们学习了解了 react 的渲染机制和生命周期,本节我们正式进入基本面试必考的核心地带 -- diff 算法,了解如何优化和复用 dom 操作的,还有...
我们在之前已经学习过 react 生命周期,但是在 16 版本中 will 类的生命周期进行了废除,虽然依然可以用,但是需要加上 UNSAFE 开头,表示是不安...
上一小节我们学习了 react 中类组件的优化方式,对于 hooks 为主流的函数式编程,react 也提供了优化方式 memo 方法,本小节我们来了解下它的用...
开源不易,感谢你的支持,❤ star me if you like concent ^_^
hel-micro,模块联邦sdk化,免构建、热更新、工具链无关的微模块方案 ,欢迎关注与了解
本文主题围绕concent的setup和react的五把钩子来展开,既然提到了setup就离不开composition api这个关键词,准确的说setup是由...
ReactsetState的执行是异步还是同步官方文档是这么说的setState()doesnotalwaysimmediatelyupdatethecomponent.Itmaybatchordefertheupdateuntillater.Thismakesreadingthis.staterightaftercallingsetState()apotentialpitfall.Instead,usecom