DOM是Web平台编程模型的基础,其设计和性能影响了浏览器管道的其余部分。然而,它的历史和进化远离一个简单的故事。
在过去的三年里,我们已经开始了在微软边缘的中型中的飞行中重构,我们注意到了一个现代化的架构,提供了更好的现实性能和减少复杂性。在这篇文章中,我们将通过Internet Explorer和Microsoft Edge中的DOM的历史,以及我们最近的工作来实现DOM树的影响,这已经导致Windows 10创建者更新的性能大大提高。
我们认为“DOM”是真正的几个子系统的合作。在Microsoft Edge中,这包括JS绑定,事件,编辑,拼写检查,HTML属性,CSSOM,文本和其他所有工作。在这些子系统中,DOM“树”位于中心。
几年前,我们开始更新到现代DOM“树”(节点连接结构)。通过现代化我们在Microsoft Edge 14完成的核心树,我们降落了一个新的基线和脚手架,以提供我们快速可靠的DOM的承诺。凭借Windows 10创作者更新和Microsoft Edge 15,我们开始的旅程开始脱颖而出。
我们只是刮伤了这一点,但想借此机会来盯着一点,并分享这一旅程的一些内部细节,从DOM的奥术历史开始,展示了我们的一些成就。
当Web开发人员今天想到DOM时,他们通常会想到一棵看起来像这样的树:
然而,很好而简单(和显而易见)这似乎,Internet Explorer的DOM实施的现实更复杂。
简单地说,Internet Explorer的DOM是为90年代的网络设计的。当设计原始数据结构时,Web主要是文档查看器(具有少数动画GIF和抛出的其他图像)。因此,算法和数据结构更像是您可能看到为像Microsoft Word这样的文档查看器供电的那些。在Web的早期召回,没有JavaScript允许脚本脚本,所以我们所知道的DOM树不存在。文本是国王,DOM的内部结构围绕快速,高效的文本储存和操纵设计。内容编辑(WYSIWYG)已经是当时的特征,操作范例围绕编辑光标,用于字符插入和有限的格式。
由于其以文本为中心的设计,DOM的原理结构是文本备份商店,一个复杂的文本阵列系统,可以有效地分割并加入最小或没有内存分配。背衬商店将文本和标签表示为线性进展,可通过全局索引或字符位置(CP)寻址。在给定CP处插入文本是高效的,并复制/粘贴了一系列文本,通过有效的“拼接”操作集中处理。下图在目视示出了将包含“Hello World”的简单标记装入文本备份商店,以及如何为每个字符和标记分配CPS。
为了存储非文本数据(例如格式化和分组信息),从备用商店单独维护另一组对象:双链接的树位置列表(Treepos对象)。 TreePOS对象是HTML源标记中标记的语义等效 - 每个逻辑元素由Begin和End TreePOS表示。这种线性结构使其在深度第一预订遍历中遍历整个DOM“树”(根据每个DOM搜索API和CSS /布局算法所需)。稍后,我们将TreePOS对象扩展到包括另外两种“位置”:TREDATAPOS(用于指示文本的占位符)和指示器(用于指示卡特,范围边界点,最终为“新”的功能,如生成的内容节点)。
每个TreePOS对象还包括CP对象,它充当标记的全局序索索引(对遗留文档等内容有用)。 CPS用于从TreePOS到文本备份商店,轻松比较节点顺序,甚至通过减去CP索引找到文本的长度。
要将其全部绑在一起,一个TreeNode绑定的树位置对并建立了JavaScript Dom预期的“树”层次结构,如下所示。
CPS的基础造成了旧DOM的大部分复杂性。对于整个系统正常工作,CPS必须是最新的。因此,在每个DOM操作之后更新CPS(例如,输入文本,复制/粘贴,DOM API操作,即使单击页面 - 在DOM中设置插入点)。最初,DOM操纵主要由HTML解析器或用户操作驱动,并且CPS总是最新的模型非常合理。但随着JavaScript和DHTML的兴起,这些操作变得更加常见和频繁。
为了补偿,添加了新的结构以使这些更新有效,并且SPLAY TREEN出生,将重叠系列的树连接添加到Treepos对象上。增加的复杂性有助于性能 - 首先;可以使用O(log n)速度来实现全局CP更新。然而,SPLAY树实际上仅针对重复的本地搜索优化(例如,对于DOM树中的一个地方围绕一个地方的更改),并且没有证明是JavaScript的一致好处及其更随机访问模式。
另一个设计现象是,先前提到的“剪接”操作处理复制/糊剂,延长了处理所有树突变。核心“拼接发动机”的工作三个步骤,如下图所示。
在步骤1中,通过从操作开始到结束的开始,发动机将“记录”拼接。然后将拼接记录包含此操作的命令指令(在浏览器的撤消堆栈中重复使用的结构)。在步骤2中,从树中删除与操作相关联的所有节点(即,trapode和treepos对象)。请注意,在IE DOM树中,TREENODE / TEADPOS对象与脚本引用的元素对象不同,以便于重叠标记,因此删除它们不是功能问题。最后,在步骤3中,拼接记录用于“重放”(重新创建)目标位置中的新对象。例如,为了完成AppendChild DOM操作,拼接引擎在节点周围创建了一个范围(从TREENODE的开始Treepos到其结尾),“拼接”旧位置的范围,并创建了新节点以表示节点及其儿童在新位置。您可以想象,除了算法的低效率之外,这会产生大量内存分配流失。
这些只是Internet Explorer DOM的复杂性的一些例子。要增加伤害伤害,旧DOM没有封装,因此来自解析器的代码一直到显示系统的依赖于CP / TreePOS依赖项,这需要许多Dev-octor来解脱。
复杂性具有错误,DOM代码库是可靠性责任。根据内部调查,从IE7到IE11,大约28%的所有IE可靠性错误源自核心DOM组件中的代码。这种复杂性也表现为敏捷税,因为每个新的HTML5功能都变得更加昂贵,因为它变得更加困难地改装到现有架构中的概念。
Project Spartan的推出创造了完美的机会,使我们的DOM现代化。我们从DocModes和条件评论等平台遗迹,我们开始了大规模的重构努力。我们的第一,最关键的目标:DOM的核心树。
我们知道旧的文本中心模型不再相关;我们需要一个实际上是一个内部树的DOM树,以匹配现代DOM API的期望。我们需要拆除复杂性的层数,使其几乎无法进行性能 - 调整树和其他周围系统。最后,我们有强烈希望封装新树以避免在核心数据结构上创建交叉组件依赖关系。所有这一努力将导致具有正确模型的DOM树,原稿并准备好进入额外改进。
为了使现代DOM的过渡尽可能平稳地(并且避免在孤立中建立一个新的DOM树并试图在项目结束时下降并稳定未经测试的代码-AKA“大爆炸集成”的定义,我们在三个阶段将现有的代码库转换为。该项目的第一阶段定义了与相应的API和合同的树组件边界。我们选择将API设计为在节点上运行的“读者”和“写入器”功能。而不是看起来像这样的API:
此API设计劝阻呼叫者以自己的状态思考树对象作为演员。结果,树对象仅是API中的标识,允许更强大的合同和隐藏代表细节,其证明在阶段3中有用。
第二阶段迁移了依赖于传统树内部的所有代码,以使用新建立的组件边界API。在此迁移期间,树API的实现将继续由遗留结构提供支持。这项工作花了最多的时间,并且是最少的迷人;花了几年的时间来解开旧树结构的消费者并适当地封装树。以这种方式暂存,让我们通过我们经过经过全面的增量变化,我们发布EdgeHTML 12和13,而不会扰乱运输计划。
在第三个和最终阶段,使用新的树组件边界API的所有外部代码,我们开始重新推荐并替换核心数据结构。我们统一对象(例如,单独的Treepos,treeNode和元素对象),删除了splay树和剪接引擎,删除了Pointerpos对象的概念,并删除了文本备份存储(名称几个)。最后,我们可以摆脱CP的代码。
新的树结构简单直截了当;它使用四个指针而不是通常的五个来维护连接:父,第一孩子,下一步和以前的兄弟(最后一个孩子被计算为父母的第一孩子的先前兄弟),我们可以隐藏我们背后的最后一个孩子优化TREREADER API而不更改单个来电者。重新安排树是快速效率的,我们甚至在公共DOM API上看到了CPU性能的一些改进,这对重构工作的副作用很好。
通过新的DOM树,可靠性也显着提高,从所有可靠性问题的28%下降到大约在10%左右,同时提供减少时间花费和改善团队敏捷性的二次效益。
虽然这感觉就像我们的旅程结束,其实这只是一个开始。使用我们的DOM树API到位并由一棵简单的树提供动力,我们将注意力转向包括DOM的其他子系统,眼睛有两种效率低下:子系统内的效率低下,它们之间的低效通信。
例如,我们顶级慢的DOM API之一(即使在DOM树工作之后)历来是QuerySelectorall。这是一个通用搜索API,并使用选项引擎搜索DOM以获取特定元素。毫不奇怪地,许多搜索涉及特定的元素属性作为搜索条件(例如,元素的ID或其类标识符之一)。一旦搜索代码输入了属性子系统,它就遇到了一个全新的效率低下,与新DOM树寻址的全部无关。
对于属性子系统,我们正在简化元素内容属性的存储机制。在Web的早期,DOM属性主要指向浏览器关于如何显示一块标记的浏览器。这是一个很好的例子是colspan属性:
Colspan对浏览器具有语义含义,因此必须被解析。鉴于页面并非非常动态返回,那么属性通常会像枚举处理,即创建了一个属性系统,这些系统围绕急切解析优化以用于格式化和布局。
但是,当今的应用模式,非常使用像ID,类和数据 - *这样的属性,这些属性是较少的浏览器指令和更像通用存储:
因此,我们推迟超出存储字符串所需的最小的最小工作。此外,由于UI框架经常鼓励跨元素的重复的CSS类,因此我们计划雾化字符串以减少内存使用率并提高API等QuerySelector中的性能。
虽然我们仍然有足够的工作计划,但是Windows 10创作者更新,我们很乐意分享我们取得了重大进展!
可靠地测量和提高性能艰难,基准的陷阱良好记录。为了获得可能的浏览器性能的最整体视图,Microsoft Edge团队使用用户遥测,控制测量真实世界场景以及合成基准的组合来指导我们的优化。
用户遥测“用宽刷涂料”,但根据定义测量最有影响力的工作。以下是我们用户群中的FirstChild API的构建跟踪的示例。此数据不可直接可操作,因为它没有提供性能调整所需的API调用的所有详细信息(即,DOM树)所需的所有细节,但这是用户体验的唯一直接测量,并且可以提供规划和回顾的反馈。
我们突出了我们的表现实验室和瞬间测量浏览器性能的细节,而实验室的测试本身和实验室的硬件已经改变,方法仍然是相关的。通过在复杂的网站和应用程序中捕获和重播现实世界的用户方案,如Bing地图和Office 365,我们不太可能在不受用户受益的狭隘优化中溢出。此图表是我们对Bing映射上的模拟用户的报告的示例。每个数据点都是浏览器的构建,并且悬停提供有关测量统计分布的详细信息,以及对调查更改的更多信息的链接。
我们的表现实验室的基本责任是提供测试和评估代码变更和实施选项所需的可重复性。这种重复性也用作合成基准的平台。
在基准类别中,我们最令人兴奋的改进是车速表。 Liqueometer使用Todomvc应用程序模拟几个流行的Web框架,包括ember,骨干,jquery,角度和反应。在适当的DOM树和其他浏览器子系统中的其他改进,如Chakra JavaScript引擎,运行速度计基准的时间减少了30%;在创作者更新中,我们的性能焦点净化了35%的另一种改进(请注意,车速表的得分是速度的衡量标准,因此是时间的逆函数)。
当然,最重要的绩效指标是用户的看法,所以虽然完全不科学,我们一直很高兴看到其他人通知我们的工作!
对Microsoft的荣誉,最新的Edge版本在https://t.co/kfx8y4spdi上速度速度近50%(现在粉碎Firefox)
最新的Edge也适用于ember的2倍。最后,我可以相信的一些改变!良好的工作边缘团队。 ???? pic.twitter.com/nj6qld4atw.
我们还没有完成,我们知道Microsoft Edge尚未最快的车速表基准。我们的分数将继续改善我们的绩效工作的副作用,我们将使DEV社区保持更新我们的进展。
快速的DOM对于当今的Web应用程序和经验至关重要。 Windows 10创建者更新是第一系列版本的第一个专注于重新归档DOM树的性能。 与此同时,我们将继续提高我们的性能遥测和社区资源,如CSS使用和API目录。 我们刚刚开始划伤我们的新DOM树的表面,并且前方仍然存在很长的旅程,但我们很高兴看到它引导并与您分享它! 谢谢!