2004 – 字符拼接时代
时光倒流,在 Facebook 的晚期,马克·扎克伯格还在他的宿舍里,应用 PHP 构建网站的形式是应用字符串连贯。事实证明这是一个十分好的建站形式,无论你是后端,前端,甚至没有编程齐全没有教训,都能够建一个大网站
这种编程形式的惟一问题是它不平安。如果你应用这个准确的代码,攻击者能够执行任意 Javascript。这对 Facebook 尤其不利,因为此代码将在用户上下文中执行。这样你基本上能够接管用户帐户
如果你什么都不做,你就很软弱。更蹩脚的是,对于大多数输出,它实际上是将为开发该性能的开发人员出现良好的成果。所以很少激励他 / 她增加适当的本义
而且,真正蹩脚的个性是,您须要在数百名工程师编写的数百万行代码中蕴含每个调用站点,以确保安全。犯了一个谬误?你的账户可能会被接管
防止这种不可能的状况的一个想法是,不论产生什么,都要防止所有。可怜的是,它不太管用,如果您对字符串进行双重本义,它将显示控制字符。如果您不小心本义标记,那么它将向用户显示 html!
2010 – XHP
咱们在 Facebook 提出的解决方案是扩大 PHP 的语法以容许开发人员编写标记。在这种状况下 <ul / > 不再在字符串中
当初,标记的所有内容都是应用不同的语法编写的,因而咱们晓得在生成 HTML 时不要回避它。其余所有内容都被视为不受信赖的字符串并主动本义。咱们能够在保障平安的同时放弃开发的便利性
XHP 一经推出,不久人们就意识到他们能够创立自定义标签。事实证明,它们能够让您通过组合大量这些标记轻松构建十分大的应用程序。这是语义 Web 和 Web 组件概念的一个实现
咱们开始在 Javascript 中做越来越多的事件以防止客户端和 server 之间的提早,咱们尝试了很多技术,比方领有一个跨浏览器的 DOM 库和一个数据绑定办法,但没有一个对咱们真正无效
鉴于以后的世界情况,前端工程师 Jordan Wake
向他的经理提出了以下想法:将 XHP 移植到 Javascript。他以某种形式开始实际 6 个月,以便证实这个概念。
我第一次据说这个我的项目时,我想,相对不可能实现,但在难得的机会中,这将是微小的胜利。当我终于开始应用它时,我立刻开始传福音
2013 – JSX
第一个工作是编写反对这种奇怪的 XML 语法的 Javascript 扩大。咱们 在 Facebook 应用 Javascript 转换实现了已有一段时间了。在这个例子中,我正在应用另一种办法编写下一个 JavaScript 规范 ES6 的函数。
实现 JSX 花了大概一周的工夫,这并不是 React 最重要的局部,更有挑战性的是重现 PHP 的更新机制,真的很简略。
PHP — 有什么变动吗?从新渲染所有。咱们能够让它足够快吗?
每当有任何变动时,您都会转到一个新页面并取得一个残缺的新页面。从开发人员的角度来看,这使得编写应用程序非常容易,因为您不用放心变动,也不用在 UI 发生变化时确保所有内容都同步
然而,每个人都会问的问题…它会十分慢
React — 不仅速度够快,它通常比以前的实现更快
通过 2 年的生产应用,我能够自信地说,它比大多数产品快得多,咱们替换它的代码。在本次演讲的其余部分,我将解释使之成为可能的重大优化
阿基姆·德梅尔 — “You need to be right before being good”
我在学校的老师已经说过,你须要先做对,而后能力做好。他的意思是如果您试图构建性能良好的货色,那么如果您首先构建一个简略但无效的实现并迭代性能,而不是从一开始就尝试以最佳形式构建它,那么您胜利的几率会高得多
Naive
所以,让咱们尝试利用他的倡议。咱们首先要实现最简略的版本。无论何时如果有任何变动,咱们将构建一个全新的 DOM 并替换旧的 DOM
DOM is Stateful
这是一种运行状况,但有很多边缘状况。如果你销毁 DOM 将失落以后聚焦的元素和光标,文本抉择和滚动地位也是如此。这实际上意味着 DOM 节点实际上蕴含状态。
第一次尝试是试着复原那些状态,咱们会记住焦点输出,聚焦新元素,光标和滚动地位雷同。可怜的是,这不足够
如果您应用的是 mac 并进行滚动,您将有惯性滑动。后果是没有用于读取或写入滚动惯性的 Javascript Apl。对于 iframe,如果它来自另一个 iframe,则状况更糟,安全策略实际上不容许您查看外面的内容,因而您不能复原它。DOM 不仅是有状态的,而且还蕴含暗藏状态
Reuse Nodes
为了解决这个问题,咱们的想法不是销毁 DOM 并从新创立一个新的 DOM,而是重用在两个渲染之间放弃不变的 DOM 节点
咱们将匹配节点,而不是删除以前的 DOM 树并将其替换为新的 DOM 树,如果它们没有更改,则抛弃新的 DOM 树并保留以后在屏幕上出现的旧 DOM 树
只有咱们可能匹配节点,咱们就会反复这个过程。但在某个时刻,咱们将看到一个以前不存在的新节点。在本例中,咱们将把新的 dom 树挪动到旧的(以后在屏幕上出现)dom 树
AdonisSMU — “I tend to think of React as Version Control for the DOM”
咱们当初对 React 的工作形式有了一个大抵的理解,但没有具体的打算
这是我从帽子里拿出类比卡的那一刻
回到编程的黑暗时代,如果你想让他人尝试你的代码,你会创立一个 zip 并发送给他。如果您更改了任何内容,您将发送一个新的 zip 文件
版本控制呈现了,它的工作形式是,它获取代码的快照并生成一个渐变列表,如“删除那 5 行”、“增加 3 行”、“替换这个单词”…应用 diff 算法
这正是 React 所做的,但应用 DOM 作为输出而不是文本文件
Optimal Diff — O(n^3)
因而,作为一名优良的工程师,咱们钻研了树的 diff 算法,发现最优解在 O(n)中
假如咱们有一个蕴含 10,000 个 DOM 节点的页面。它很大但并非不可设想。为了失去一个数量级,咱们假如咱们能够在一个 CPU 周期内实现一个操作(不会产生),并且有一台 1GHz 的机器
计算差别须要 17 分钟!咱们不能用那个。。。
然而,不要胆怯,咱们晓得仍处于咱们须要正确的阶段。那么咱们钻研它的工作形式
(1)比照新树中的每个节点,
(2) 将再次匹配旧树的每个节点
(3) 匹配操作对整个子树进行操作。在这里,咱们失去了三个嵌套循环
认真想想,在 web 应用程序中,咱们很少须要将元素挪动到页面中的任何其余地位。我想到的惟一一个例子是拖放,但这并不常见。
挪动元素的惟一时候是在子元素之间您常常增加 / 删除 / 挪动列表中的元素
子元素嵌套
所以,咱们能够通过子元素来计算差别。咱们从其根开始并将其与其余根做匹配
并为所有匹配的子元素这样做。咱们从一个可怕的大 O(n 变成很多 O(m
咱们尝试变得更好更快
事实证明,咱们不能间接应用 Levenstein 间隔算法
Identity
为了理解起因,最好的办法是通过一个小例子。咱们看到第一个渲染有三个输出,而下一个渲染只有两个输出。问题是如何匹配它们?
直观的反馈是将前两个匹配在一起并删除第三个
然而,咱们也能够删除第一个并将最初两个匹配在一起
一个不太显著但依然齐全无效的解决方案是删除所有以前的元素并创立两个新元素。所以在这一点上,咱们没有足够的信息来正确地进行匹配,因为咱们心愿可能解决上述所有用例
一种想法是不仅应用标签名称而且应用属性。如果它们前后相等,那么咱们进行匹配
事实证明这不适用于 value 属性。如果您尝试输出“oscon”,那么两者将是不同的输出焦点
另一个更有心愿的属性是 id 属性。在表单上下文中,它通常蕴含输出对应的模型的 id
当初,咱们可能胜利匹配两个列表!(您是否留神到它与我之前展现的三个示例相比又一次匹配?)
然而,如果您通过 AJAX 提交表单而不是让浏览器来提交,则不太可能将该 id 属性放入 DOM 中。
React 引入 key 属性。它惟一的工作就是帮忙 diff 算法进行匹配
子元素嵌套
事实证明,咱们能够在 O(n)中通过哈希表应用比 O(n 快得多的 keys 来实现匹配
所以,如果咱们把所有的局部 O(m)相加,咱们失去了 O(n)的总复杂度。不可能有更好的复杂性
Let the goodness begin!
此时,咱们曾经有了一个正确的解决方案,咱们当初能够开始施行所有很酷的优化以使其疾速运行
如果你对 JS 利用做过任何优化,你可能据说 DOM 很慢。Stack Exchange
上的 Rafal
做了一个很好的阐明。如果你枚举一个空 div 的所有属性,你会看到很多!
之所以有这么多属性,是因为浏览器渲染管道的很多步骤都应用了一个 DOM 节点。
浏览器首先查看 CSS 规定并找到与该节点匹配的规定,并在此过程中存储各种元数据以使其更快。例如,它保护一个 id 到 dom 节点的映射。
而后,它采纳这些款式并计算布局,其中蕴含屏幕中的地位和定位。同样,大量的元数据。它将尽可能防止从新计算布局,并缓存以前计算的值。
而后,在某些时候,您实际上是 CPU 或 GPU 上的缓冲区。
所有这些步骤都须要起媒介作用示意并应用内存和 CPU。浏览器在优化整个管道方面做得十分好
Virtual DOM
然而,如果您考虑一下 React 中产生的事件,咱们只会在 diff 算法中应用那些 DOM 节点。所以咱们能够应用一个更轻的 JavaScript 对象,它只蕴含标签名称和属性。咱们称之为虚构 DOM。
diff 算法生成一个 DOM 渐变列表,与版本控制输入文本渐变的形式雷同
咱们能够利用到真正的 DOM 上。而后咱们让浏览器执行所有优化的管道。咱们将低廉但须要的 DOM 渐变数量缩小到最低限度
Batching
来自可汗学院的 Ben Alpert
通过批处理操作解决了这个问题。
为了通知 React 某些事件产生了变动,您能够在元素上调用 setState。React 只会将元素标记为脏元素,但不会立刻计算任何内容。如果你在同一个节点上屡次调用 setState,它的性能也会一样
一旦初始事件齐全流传……
咱们能够从上到下从新渲染元素。这十分重要,因为它确保咱们只渲染元素一次
当初所有元素都已从新渲染到虚构 DOM,咱们将其提供给输入 DOM 渐变的 diff 算法。在此过程中,咱们无需从 DOM 中读取任何内容。React 是只写
Subtree Re-rendering
一开始我说心智模型是“当任何事件发生变化时从新渲染所有”。这在实践中并不完全正确。咱们只从已被 setState 标记的元素从新渲染子树。
当您开始将 React 集成到您的应用程序中时,通常的模式是在树内具备非常少的状态,因而 setState 代价非常低,因为它们只从新出现 UI 的一小部分
Pruning
当更多的应用程序被转换时,状态往往会回升,这意味着当产生任何变动时,您正在从新渲染应用程序的更大部分。
为了在性能方面加重这种影响,您能够实现 shouldComponentUpdate
,它带有前一个和下一个state/props 能够说:“你晓得吗,什么都没有扭转,让咱们跳过从新渲染这个子树”
这使您能够精简大部分的树并从新取得性能
shouldComponentUpdate?
咱们在开源版本中引入了 shouldComponentUpdate
,但咱们不太分明如何正确实现它。
问题是在 JavaScript 中你常常应用对象来保留状态并间接扭转它。这意味着状态的上一个和下一个版本是对对象的雷同援用。因而,当您尝试将上一个版本与下一个版本进行比拟时,即便产生了一些变动,它也会说前后状态统一。
纽约时报的 大卫诺伦
想出了一个很好的解决方案。在 ClojureScript
中,所有大多数值都是不可变的,这意味着当你更新一个时,你会失去一个新对象,而旧的则放弃不变。这与 shouldComponentUpdate
配合得很好。
他在 ClojureScript
中在 React 之上编写了一个名为 Om
的库,该库应用不可变的数据结构来默认实现 shouldComponentUpdate
可怜的是,应用不变的数据结构须要一个微小的思维飞跃,而每个人还没有做好筹备。所以当初和可预感的将来 React 必须在没有它们的状况下工作,因而默认状况下无奈实现 shouldComponentUpdate
。
作为代替,咱们刚刚公布了一个性能工具。您在应用程序中玩了一段时间,每次从新渲染组件时,如果 diff 没有输入任何 DOM 变动,那么它就会记住渲染所花的工夫。最初,您会失去一个很好的表,它告诉您哪些组件将从 shouldComponentUpdate 中受害最大!
通过这种形式,您能够将其放在几个要害地位并取得最大性能劣势
Conclusion
在本次演讲中,咱们介绍了 React 正在做的四种优化:差别算法、虚构 DOM、批处理和精简。我心愿它能说明它们存在的起因以及它们如何工作。
React 用于构建咱们的桌面网站、挪动网站和 Instagram 网站。它在 Facebook 十分胜利,以至于基本上所有新的前端产品都是应用 React 编写的。这不是咱们只是在外部工具或小性能中应用的我的项目,这是每个月有数亿人应用的 Facebook 主页应用的!
因为咱们在开源会议上,我想以反思一下作为完结
咱们在 2010 年开源了 XHP,但咱们在这方面做得十分蹩脚,咱们在 4 年内只写了一篇博文。咱们没有加入会议来解释它,编写文档……然而,在 Facebook 外部,咱们十分喜爱它并在任何中央应用它。
去年咱们开源 React 时,难度要大得多,因为咱们必须同时解释 XHP 的益处以及咱们必须做的所有疯狂优化能力使其在客户端上运行。
咱们常常议论开源的益处。这是一个很好的揭示,不开源您的核心技术会使其余我的项目的开源变得更加艰难
参考资料
OSCON – React Architecture