前端性能的本质是什么

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

那么,当咱们在谈论性能时,到底在说什么?基于 React 框架开发的应用,在性能上又有哪些特色?react

前端性能的本质是什么,你有真正了解吗?

 

性能问题本质git

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

浏览器解析渲染 DOM Tree 和 CSS Tree,解析执行 JavaScript,几乎全部的操做都是在主线程中执行。由于 JavaScript 能够操做 DOM,影响渲染,因此 JavaScript 引擎线程和 U I线程是互斥的。换句话说,JavaScript 代码执行时会阻塞页面的渲染。web

经过下面的图示来进行了解:算法

前端性能的本质是什么,你有真正了解吗?

 

图中的几个关键角色:promise

Call Stack:调用栈,即 JavaScript 代码执行的地方,Chrome 和 NodeJS 中对应 V8 引擎。遵循 LIFO(last-in-first-out)原则。当执行完当前全部任务时,栈为空,等待接收 Event Loop 中 next Tick 的任务。浏览器

Browser APIs:这是链接 JavaScript 代码和浏览器内部的桥梁,使得 JavaScript 代码能够经过 Browser APIs 操做 DOM,调用 setTimeout,AJAX 等。网络

Event queue: 每次经过 AJAX 或者 setTimeout 添加一个回调时,回调函数会加入到 Event queue 当中。前端工程师

Job queue: 这是预留给 promise 且优先级较高的 queue,表明着“稍后执行这段代码,可是在 next Event Loop tick 以前执行”。它属于 ES 规范,注意区别对待,这里暂不展开。

Next Tick: 表示调用栈 call stack 在下一 tick 将要执行的任务。它由一个 Event queue 中的回调,所有的 job queue,部分或者所有 render queue 组成。注意 current tick 只会在 Job queue 为空时才会进入 next tick。这就涉及到 task 优先级了,可能你们对于 microtask 和 macrotask 更加熟悉。

Event Loop: 它会“监视”(轮询)call stack 是否为空,call stack 为空时将会由 Event Loop 推送 next tick 中的任务到 call stack 中。

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

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

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

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

咱们通常有两种方案突破上文提到的瓶颈:

将耗时高、成本高的长任务切片,分红子任务,并异步执行

这样一来,这些子任务会在不一样的 call stack 周期执行,进而主线程就能够在子任务间隙当中执行 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,生成 render patch 一系列复杂过程所形成的。也就是说 React 著名的调度策略 -- stack reconcile 是 React 的性能瓶颈。

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

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

咱们来看一个典型的场景,来自文章“React的新引擎—React Fiber是什么?”

(http://www.infoq.com/cn/articles/what-the-new-engine-of-react)

这个例子会在页面中建立一个输入框,一个按钮,一个 BlockList 组件。BlockList 组件会根据 NUMBER_OF_BLOCK 数值渲染出对应数量的数字显示框,数字显示框显示点击按钮的次数。

前端性能的本质是什么,你有真正了解吗?

 

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

显而易见,这样的用户体验并很差。

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

前端性能的本质是什么,你有真正了解吗?

 

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

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

此处场景内容选自文章“React的新引擎—React Fiber是什么?”

React 性能——React Fiber

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

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

前端性能的本质是什么,你有真正了解吗?

 

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

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

React结合Web Worker

关于 Web Worker 的概念此文再也不赘述,你们能够访问 MDN 地址进行了解。咱们聚焦思考点:若是让 React 接入 Web Worker 的话,切入点在哪里,如何实施?

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

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

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

也确实有人提出了这样的想法,请参考 React 仓库第 #3092 号 Issue,这样的提议遭到了 React 官方的礼貌回绝:

“Relay in a worker on the other hand seems very plausible.”

具体缘由能够在此 issue 中找到,内容不少,也吸引来了 Dan Abramov 的现身说法,固然若是我是 React 库的开发者,我也不会接受这样的变更。不过这并不妨碍咱们让 React 结合 Worker 作试验。

Talk is cheap, show me the code, and demo: 读者能够访问

http://web-perf.github.io/react-worker-dom/,

该网站分别用原生 React 和接入 Web Worker 版 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,因为 web Worker 不能直接操做 DOM,也就是说不能监听浏览器事件。所以全部事件一样都在主线程中处理,转化为虚拟事件并传递给 worker 线程,也就意味着全部关于建立虚拟事件的操做仍是都在主线程中进行,一个可能改善的方案是,能够直接将原始事件传递给 worker,由 worker 来生成模拟事件并冒泡传递。

关于 React 结合 worker 还有不少值得深挖的内容,好比事件处理方面 preventDefault 和 stopPropogation 的同步性;使用 multiple worker(一个以上 worker)探究等,若是读者有兴趣,我会专门写篇文章介绍。

Redux和Web Worker

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

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

  • 一个实时每 16 毫秒,显示计数(每秒增长 1)的 blinker 模块;
  • 一个定时每 500 毫秒,更新背景颜色的 counter 模块;
  • 一个永久往复运动的 slider 模块;
  • 一个每 16 毫秒翻转 5 度的 spinner 模块
前端性能的本质是什么,你有真正了解吗?

 

这些模块都定时频繁地更新 DOM 样式,进行渲染。正常状况下,当 JavaScript 主线程进行 N-皇后计算时,这些渲染过程都将被卡顿。

若是将 N-皇后计算放置到 worker 线程,咱们会发现 demo 展示了使人惊讶的性能提高,彻底丝滑毫无卡顿。

以下图,左边为正常版本,不出意外出现了页面卡顿,右侧是介入 worker 以后的应用:

前端性能的本质是什么,你有真正了解吗?

 

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

相关文章
相关标签/搜索