前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >漫谈前端性能本质 突破React应用瓶颈

漫谈前端性能本质 突破React应用瓶颈

作者头像
用户1682855
发布2018-09-29 11:45:59
1.2K0
发布2018-09-29 11:45:59
举报
文章被收录于专栏:前沿技墅前沿技墅前沿技墅

本文作者

侯策:硕士毕业于法国国立高等电信学校。曾任职于BePATIENT集团,负责互联网+医疗平台的研发。曾任职于法国能源和苏伊士集团,参与欧洲天然气运输和费用系统的研发。2015年回国加入百度知识搜索部,负责多个产品线的大型技术迭代。行业之外是一名国家二级运动员(足球项目),曾组织过赴北非撒哈拉地区看望孤儿等慈善活动。

颜海镜:知名技术博主,开源达人,常以歪脖无脸男形象作为头像活跃于各大技术网站,经过多年沉淀,专注Web前端开发,先后任职于金山、百度、美团点评,负责前端开发工作。

性能一直以来是前端开发中非常重要的话题。随着前端能完成的事情越来越多,浏览器的能力也被无限放大和利用:从Web游戏到复杂单页面应用,从Node.js服务到Web VR/AR和数据可视化,前端工程师总在突破极限。随之而来的性能问题有的被迎刃而解,有的则成为难以逾越的盾墙。

那么,当我们在谈论性能时,到底在说什么?基于React框架开发的应用,在性能上又有哪些特点?在这篇文章中,我们将从浏览器和JavaScript引擎角度来剖析前端性能,同时创新React,充分利用浏览器能力突破局限。

性能问题本质和阿喀琉斯之踵

事实上,性能问题多种多样:瓶颈可能出现在网络传输过程,造成前端数据延迟;也可能出现在移动Hybrid应用中,由于WebView容器带来了瓶颈和限制。但是在分析性能问题时,都逃不开一个概念——JavaScript单线程。

浏览器解析渲染DOM Tree和CSS Tree,解析执行JavaScript,几乎所有的操作都是在主线程中执行的。因为JavaScript可以操作DOM,影响渲染,所以JavaScript引擎线程和UI线程是互斥的。换句话说,JavaScript代码执行时会阻塞页面渲染,如下图所示。

图中的几个关键角色如下。

  • Call Stack:调用栈,即JavaScript代码执行的地方,Chrome和Node.js中对应V8引擎。遵循LIFO(last-in-first-out)原则,当执行完当前所有任务时,栈为空,等待接收Event Loop中nextTick的任务。
  • Browser APIs:这是连接JavaScript代码和浏览器内部的桥梁,使得JavaScript代码可以通过Browser APIs操作DOM,调用setTimeout、AJAX等。
  • Event queue:每次通过AJAX或者setTimeout添加一个回调时,回调函数都会加入Eventqueue当中。
  • Job queue:这是预留给promise且优先级较高的queue,代表“稍后执行这段代码,但是在nextEvent Loop tick之前执行”。它属于ES规范,这里暂不展开。
  • Next Tick:表示调用栈call stack在下一tick将要执行的任务。它由一个Event queue中的回调,全部的job queue,部分或者全部renderqueue组成。注意current tick只会在Job queue为空时才会进入nexttick。这就涉及task优先级了,可能大家对于microtask和macrotask更加熟悉。
  • Event Loop:它会“监视”(轮询)call stack是否为空,callstack为空时将会由Event Loop推送next tick中的任务到callstack中。

在浏览器主线程中,JavaScript代码在调用栈call stack执行时,可能会调用浏览器的API对DOM 进行操作,也可能执行一些异步任务:这些异步任务如果是以回调的方式处理,那么往往会被添加到Event queue当中;如果是以promise方式处理,就会先放到Jobqueue当中。这些异步任务和渲染任务将会在下一个时序当中由调用栈处理执行。

理解了这些,大家就会明白:如果调用栈callstack运行一个很耗时的脚本,比如解析一个图片,callstack就会像北京上下班高峰期的环路入口一样,被这个复杂任务堵塞。进而阻塞UI响应,主线程其他任务都要排队。这时候用户点击、输入、页面动画等都没有了响应。

这样的性能瓶颈,就如同阿喀琉斯之踵一样,在一定程度上限制着JavaScript的发挥。

江湖救急——两方性能解药

我们一般可以采取两种方案突破上面提到的瓶颈。

  • 第一种:将耗时高、成本高的长任务切片,分成子任务,并异步执行

这样一来,这些子任务会在不同的callstack周期执行,进而主线程就可以在子任务间隙当中执行UI更新操作。设想常见的一个场景:如果我们需要渲染一个很长的列表,列表由十万条数据组成,那么相比一次性渲染全部数据内容,我们可以将数据分段,使用setTimeout API去分步处理,构建列表的工作就被分成了不同的子任务在浏览器中执行,在这些子任务间隙,浏览器得以处理UI更新。

  • 第二种:使用 HTML5 Web Worker

Web Worker允许我们将JavaScript脚本在不同的浏览器线程中执行。因此,一些耗时的计算过程我们都可以放在Web Worker开启的线程当中处理。下文会有详解。

React 框架性能剖析

社区上关于React性能的内容往往聚焦在业务层面,主要是使用框架的“最佳实践”。这里我们不谈论“使用shoulComponentUpdate减少不必要的渲染” “减少render函数中inline-function”等“老生常谈”的话题,而是从React框架实现层面分析其性能瓶颈和突破策略。

原生的JavaScript一定是最高效的,这一点毫无争议。相比其他框架,React在JavaScript执行层面花费的时间较多,这显然是因为Virtual DOM构建,以及计算 DOM diff,生成renderpatch一系列复杂过程所造成的。也就是说React著名的调度策略——stack reconcile,是React的性能瓶颈。

这并不难理解,因为UI渲染只是JavaScript调用浏览器的APIs,这个过程对所有框架以及原生 JavaScript来讲是一样的,都是黑盒执行,这一部分的性能消耗是且无法取巧的。

再看React,stack reconcile过程会深度优先遍历所有的Virtual DOM节点,进行diff。整棵VirtualDOM计算完成之后,才将任务出栈释放主线程。所以,浏览器主线程被React更新状态任务占据的时候,用户与浏览器进行任何的交互都不能得到反馈,只有等到任务结束,才能突然得到浏览器的响应。

我们来看一个典型的场景,来自文章《React的新引擎——React Fiber是什么?》,这个例子会在页面中创建一个输入框,一个按钮,一个BlockList组件。BlockList组件会根据NUMBER_OF_BLOCK数值渲染出对应数量的数字显示框,数字显示框显示点击按钮的次数。

在这个例子中,我们可以设置NUMBER_OF_BLOCK的值为100000,将其变为一个“复杂”的网页。点击按钮,触发setState,页面开始更新。此时点击输入框,输入一些字符串,比如“hi,react”。可以看到,页面没有任何的响应。等待7s之后,输入框中突然出现了之前输入的“hireact”。同时,BlockList组件也更新了。

显而易见,这样的用户体验并不好。

将浏览器主线程在这7s的performance如下图所示。

黄色部分是JavaScript执行时间,也是React占用主线程时间,紫色部分是浏览器重新计算DOM Tree的时间,绿色部分是浏览器绘制页面的时间。

三种任务,占用浏览器主线程7s,此时间内浏览器无法与用户交互。但DOM改变之后,浏览器重新计算DOMTree,重绘页面是一个必不可少的阶段(紫色绿色阶段)。主要是黄色部分执行时间较长,占用了6s,即React较长时间占用主线程,导致主线程无法响应用户输入。

React 性能——ReactFiber

React核心团队很早之前就预知性能风险的存在,并且持续探索可解决的方式。基于浏览器对requestIdleCallback和requestAnimationFrame这两API的支持,React团队实现新的调度策略——Fiber reconcile。

在应用React Fiber的场景下,再重复刚才的例子。浏览器主线程的performance如下图所示。

可以看到,在黄色JavaScript执行过程中,也就是React占用浏览器主线程期间,浏览器在也在重新计算DOM Tree,并且进行重绘,截图显示,浏览器渲染的就是用户新输入的内容。简单说,在React占用浏览器主线程期间,浏览器也在与用户交互。这显然是“更好的性能”体现。

以上是React “将耗时高的任务分段”做法,下面我们再来看另一种“民间”做法,体现WebWorker应用。

React结合WebWorker

关于Web Worker的概念此文不再赘述,大家可以访问MDN地址进行了解。我们聚焦思考点:如果让React接入Web Worker,切入点在哪里,如何实施?

众所周知,标准的React应用由两部分构成。

  • React core:负责绝大部分的复杂的VirtualDOM计算。
  • React-Dom:负责与浏览器真实DOM交互来展示内容。

那么答案很简单,我们尝试在WebWorker中运行React Virtual DOM的相关计算,即将React core放入WebWorker线程中,而不是在传统的主线程中进行。

也确实有人提出了这样的想法,请参考React仓库第#3092号Issue,这样的提议遭到了React官方的礼貌回绝:“Relayin a worker on the other hand seems very plausible.”

具体原因可以在此Issue中找到,内容很多,也吸引了Dan Abramov来现身说法,当然如果我是React库的开发者,我也不会接受这样的变动。不过这并不妨碍我们让React结合Worker做试验。

读者可以访问http://web-perf.github.io/react-worker-dom/,分别用原生React和接入WebWorker版React实现了两个应用,并对比了其性能表现。

最终结论:不能绝对说Web Worker可以对渲染速率有大幅度提升,只有当大量的节点发生变化的时,Web Worker提升渲染性能才会有一些效果。实际上,当节点数量非常少的时候,Web Worker的性能可能还不如React本身的实现。这是由worker线程和主线程之间的通信成本所致。

因此,Web Worker版本的React仍有提升空间,简单总结如下。

  • 因为Worker线程和主线程在使用postMessage通信时成本较大,因此我们可以采用batching思想减少通信的次数。 如果在每次DOM需要改变时,都调用postMessage通知主线程,不是特别明智。所以可以用batching思想,将Worker线程中计算出来的DOM待更新内容收集起来,再统一发送。这样一来,batching的粒度就很有意思了。如果我们采用比较极端的方法,令每次batching收集的变更都非常多,那么在一次batching时就给浏览器真正的渲染过程带来了压力,适得其反。
  • 使用postMessage传递消息时采用transferable objects进行数据负载。在Worker和主线程间,想要传递的数据可能不是稳定的结构,因此需要制定一个公共的协议。使用transferable objects传递信息,能够提高效率。
  • 关于Worker版syntheticEvent。 原生React中有一套Event System,在最顶层监听所有的浏览器事件,将它们转化为合成事件,传递给我们在Virtual DOM上定义的事件监听者。 由于Web Worker不能直接操作DOM,也就是说不能监听浏览器事件。因此所有事件都在主线程中处理,转化为虚拟事件并传递给Worker线程,意味着所有关于创建虚拟事件的操作都在主线程中进行。一个可能改善的方案是,可以直接将原始事件传递给Worker,由Worker来生成模拟事件并冒泡传递。

关于React结合Worker,还有很多值得深挖的内容,比如事件处理方面的preventDefault和stopPropogation的同步性;使用一个以上Worker进行探究等。

Redux和WebWorker

既然React可以接入Web Worker,状态管理工具Redux当然也能借鉴这样的思想,将Redux中reducer复杂的纯计算过程放在Worker线程里,是不是一个很好的思路?

使用 “N-皇后问题” 模拟大型计算,除了这个极其耗时的算法,页面中还运行了以下几个模块来实现渲染逻辑,如下图所示。

  • 一个实时每16ms显示一次计数(每秒增加 1)的blinker模块。
  • 一个定时每500ms更新一次背景颜色的counter模块。
  • 一个永久往复运动的slider模块。
  • 一个每16ms翻转5度的spinner模块。

这些模块都定时频繁地更新DOM样式,进行渲染。正常情况下,当JavaScript主线程进行N-皇后计算时,这些渲染过程都将卡顿。如果将N-皇后计算放置到Worker线程,我们会发现demo展现了令人惊讶的性能提升,完全丝滑,毫无卡顿。

如下图所,左边为正常版本,不出意外地出现了页面卡顿,右侧是接入Worker之后的版本,两者差距明显。

在实现层面,借助Redux库的enchancer设计,完成了抽象封装(类似中间件)。一个store enhancer,实际上就是一个颗粒化的高阶函数,最终返回值是一个可以创建功能更加强大的store的函数(enhanced store creator),这和React中的高阶组件的概念很相似,同时也类似我们更加熟悉的中间件,其实参考Redux源码,会发现Redux源码中applyMiddleware(...middlewares)的执行结果就是一个store enhancer。

————

这个Redux worker demo所采用的公共库设计思路非常有趣,关于神奇的Redux高阶内容不再展开,感兴趣的读者可以在博文视点新书《React状态管理与同构实战》中看到更多精彩内容。本书由知名技术博主侯策、颜海镜亲自执笔,得到百度公司副总裁沈抖、百度高级前端工程师董睿,以及阮一峰、狼叔、justjavac、小爝、顾轶灵等前端圈众多专家大咖的联合力荐。身处全栈开发大潮之中,面对同构技术(服务端渲染技术)这一前端开发中的趋势性技术,焉有不阅读原文一下的道理?

内容简介:React自开源以来,便以革命性的设计理念迅速颠覆了前端开发的传统意义,其倡导的组件化、状态管理、虚拟DOM等思想极大提高了前端开发效率。为了更加高效地维护React应用的数据状态,以Redux为代表的数据管理模式横空出世。

本书以React技术栈为核心,在介绍React用法的基础上,从源码层面分析了Redux思想,同时着重介绍了服务端渲染和同构应用的架构模式。书中包含许多项目实例,不仅为用户打开了React技术栈的大门,更能提升读者对前沿领域的整体认知。本书主要适合具有一定JavaScript基础的前端工程师,以及对前端开发感兴趣的相关从业人员阅读。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-09-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前沿技墅 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
消息队列 TDMQ
消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档