Lara

JavaScript 工作原理:渲染引擎及其性能优化

原文链接: blog.sessionstack.com

这是系列文章的第11篇,专门探讨 JavaScript 及其构建组件。在识别和描述核心元素的过程中,我们还分享了一些在构建 SessionStack 时使用的经验法则。SessionStack 是一个需要强大且高性能的 javaScript 应用程序,以帮助用户实时发现并重现他们的 Web 应用程序缺陷。

如果你想阅读系列的其他文章,请看下方:

到目前为止,在我们之前关于 “JavaScript工作原理” 的系列博文中,我们一直专注于 JavaScript 作为一种语言它的特性,它如何在浏览器中执行,如何优化它等等。

但是,在构建 web 应用程序时,不只是编写独立运行的 JavaScripot 代码。 你写的 JavaScript 代码需要和环境进行交互。理解这个环境,以及它是如何工作和由什么组成的将帮助你构建更好的应用程序,并能为应用程序发布后可能出现的潜在问题做好准备。

那么,我们来看看浏览器主要由哪些部分组成:

  • 用户界面:包括地址栏、后退和前进按钮、书签菜单等等。大体上,这是除了浏览网页的窗口以外浏览器所展示的部分。

  • 浏览器引擎:它处理用户界面和渲染引擎之间的交互。

  • 渲染引擎:它负责显示网页。渲染引擎解析 HTML 和 CSS,并在屏幕上显示解析后的内容。

  • 网络层:这些是不同的平台使用不同的方式实现的网络调用,如 XHR 请求,位于独立于平台的接口后面。 在本系列之前的文章中,我们更详细地讨论了网络层。

  • UI 后台:它用于绘制核心小部件,如复选框和窗口。该后台暴露了一个非平台特定的通用接口。它使用操作系统底层的UI方法。

  • JavaScript 引擎:在本系列之前的文章中,我们已进行了详细的介绍。大体就是,这是 JavaScript 执行的地方。

  • 数据持久化:你的应用程序可能需要在本地存储所有数据。支持的存储机制类型包括 localStorage,indexDB,WebSQL 和 FileSystem。

本文中,我们将重点介绍渲染引擎,因为它处理 HTML 和 CSS 的解析和可视化,而且大多数 JavaScript 应用程序会一直与之交互。

渲染引擎概况

渲染引擎主要负责在浏览器屏幕上显示请求的页面。

渲染引擎可以显示 HTML 和 XML 文件和图像。如果你使用其他的插件,该引擎还能显示不同类型的文档,如 PDF。

渲染引擎

与 JavaScript 引擎类似,不同的浏览器也使用不同的渲染引擎。下面是一些流行的渲染引擎:

  • Gecko — Firefox

  • WebKit — Safari

  • Blink — Chrome, Opera (从版本15起)

渲染过程

渲染引擎从网络层接收所请求文档的内容。

构建DOM树

渲染引擎的第一步是解析 HTML 文档,并将解析的元素转换为DOM 树 中的实际 DOM 节点。

假设你输入了以下文本:

<html>
  <head>
    <meta charset="UTF-8">
    <link rel="stylesheet" type="text/css" href="theme.css">
  </head>
  <body>
    <p> Hello, <span> friend! </span> </p>
    <div> 
      <img src="smiley.gif" alt="Smiley face" height="42" width="42">
    </div>
  </body>
</html>

该 HTML 文档的 DOM 树看起来像下面这样:

基本上,每个元素都表示为其直接所包含的所有元素的父节点,并且这是递归进行的。

构造 CSSOM 树

CSSOM 指的是 CSS 对象模型。当浏览器构建页面的 DOM 时,在 head 部分,遇到引用外部层叠样式表 theme.csslink 标签时,预计可能需要这个资源来渲染页面,它就立即为此发送了一个请求。 假设 theme.css 文件包含以下内容:

body { 
  font-size: 16px;
}

p { 
  font-weight: bold; 
}

span { 
  color: red; 
}

p span { 
  display: none; 
}

img { 
  float: right; 
}

和 HTML 一样,引擎需要将 CSS 转换成浏览器可以识别的东西— CSSOM。 下面是 CSSOM 树的样子:

你想知道为什么 CSSOM 有树形结构吗?当计算页面上任何对象的最终样式集时,浏览器从适用于该节点的最一般规则(例如,如果它是 body 元素的子级,则应用所有 body 的样式)开始,然后通过应用更具体的规则递归地优化计算出的样式。

让我们来研究一下给出的具体示例。 位于 body 元素中的 span 标签所包含的所有文本,字体大小都是 16px 并且颜色为红色。这些样式是从 body 元素继承的。 如果 span 元素是 p 标签的子元素,则会因为设置了更具体的样式,而不会显示其内容。

另外,请注意,上面的树不是完整的 CSSOM 树,它只显示了样式表中我们决定重写的样式。每个浏览器都提供了一组默认的样式,也称为 用户代理样式 — 也就是我们没有明确提供任何样式时会看到的样式。 我们的样式只是覆盖了这些默认样式。

构建渲染树

HTML 中的视觉指令与 CSSOM 树中的样式数据相结合,用于创建渲染树

你可能会问什么是渲染树?这是一个可视化元素树,按照它们在屏幕上显示的顺序构建。它是 HTML 和相应的 CSS 的可视化表示。此树的目的是使内容能够按正确的顺序进行绘制。

渲染树中的每个节点称为渲染器或WebKit中的渲染对象。

这里是上面的 DOM 和 CSSOM 树的渲染器树的样子:

要构造渲染树,浏览器大致执行以下操作:

  • 从 DOM 树的根节点开始,它遍历每个可见节点。某些节点是不可见的(例如,script 标签,meta 标签等等),并且由于它们不进行渲染输出而被忽略。还有些通过 CSS 隐藏的节点,也会从渲染树中省略。例如,上例中的 span 标签 — 在渲染树中不存在,因为我们对其设置了 display:none 属性。

  • 对每个可见节点,浏览器会找到合适的匹配 CSSOM 规则并应用它们。

  • 它发出带有内容及其计算样式的可见节点。

你可以在这里查看 RenderObject 的源代码(在webkit中): https://github.com/WebKit/webkit/blob/fde57e46b1f8d7dde4b2006aaf7ebe5a09a6984b/Source/WebCore/rendering/RenderObject.h

让我们来看看这个类的核心内容:

class RenderObject : public CachedImageClient {
  // Repaint the entire object.  Called when, e.g., the color of a border changes, or when a border
  // style changes.

  Node* node() const { ... }

  RenderStyle* style;  // the computed style
  const RenderStyle& style() const;

  ...
}

每个渲染器代表一个通常与某节点的 CSS 盒子相对应的矩形区域。它包含了一些几何信息,如宽,高和位置。

渲染树的布局

当渲染器被创建并添加到树中时,它是没有位置和大小的。而计算这些值就称为布局。

HTML 使用基于流的布局模型,这意味着它大多数情况下可以一次性计算出几何结构。坐标系是相对于根渲染器的,使用顶部和左侧坐标。

布局是一个递归的过程 — 它从对应于 HTML 文档 <html> 元素的根渲染器开始。布局通过部分或整个渲染器的层级继续递归,为每个依赖它的渲染器计算几何信息。

根渲染器的位置为 0,0,其尺寸与浏览器窗口(即视区)的可见部分大小相同。

布局过程的开始意味着给每个节点赋予应该在屏幕中显示的精确坐标。

绘制渲染树

在此阶段,将遍历渲染器树,并调用渲染器的 paint() 方法在屏幕上显示内容。

绘制可以是全局的或递增的(类似于布局):

  • 全局的 — 重新绘制整个树。

  • 递增的 —  只有部分渲染器以不会影响整个树的方式改变。渲染器使屏幕上的矩形无效。这将导致操作系统将其视为需要重绘的区域并生成一个 paint 事件。操作系统通过一种将多个区域合并为一个区域的智能方式来实现这一点。

通常来说,了解绘制是一个渐进的过程是非常重要的。为了更好的用户体验,渲染引擎会尽可能快地将内容显示在屏幕上。它不会等到所有的 HTML 都被解析后才开始构建和绘制渲染树。部分内容将被解析并显示,同时该过程会继续处理来自网络的其余内容项。

处理脚本和样式表的顺序

当解析器到达 <script> 标签时,将立即解析并执行脚本。在脚本执行之前,将停止对文档的解析,这意味着该过程是同步的

如果脚本是外部的,那么首先必须从网络中获取它(也是同步的)。在获取完成之前,所有解析都将停止。

HTML5 添加了一个选项,将脚本标记为异步脚本,以便由其他线程解析和执行。

优化渲染性能

如果你想优化你的应用程序,你需要关注五个主要方面。以下是您可以控制的方面:

  1. JavaScript —  在之前的文章中,我们讨论了编写优化代码的主题,优化的代码不会阻塞 UI,内存效率高等等。当涉及到渲染时,我们需要考虑 JavaScript 代码与页面上的 DOM 元素交互的方式。JavaScript 可以在 UI 中创建大量的更改,特别是在单页应用中。

  2. 样式计算 — 这是根据匹配的选择器确定哪个 CSS 规则应用于哪个元素的过程。一旦规则被定义,将应用这些规则并计算每个元素的最终样式。

  3. 布局 — 一旦浏览器知道哪些规则应用于某个元素,它就可以开始计算元素占用的空间以及它在浏览器屏幕上的位置。web的布局模型定义了一个元素可以影响其他元素。例如,<body> 的宽度可以影响其子级的宽度等。这意味着布局过程是计算密集型的并且绘制是在多层上完成的。

  4. 绘制 — 这是填充实际像素的步骤。这个过程包括绘制文本、颜色、图像、边框、阴影等 — 每个元素的每个可视部分。

  5. 合成 — 由于页面部分被绘制到潜在的多个层中,所以它们需要以正确的顺序绘制到屏幕上,以便页面正确渲染。这一点非常重要,特别是对于重叠的元素。

优化 JavaScript

JavaScript 经常触发浏览器中的视觉变化。尤其是构建单页应用时。

下面是一些关于你可以优化 JavaScript 的哪些部分来改进渲染的建议:

  • 避免使用 setTimeoutsetInterval 进行视觉更新。它们将在帧的某个不确定时刻调用“callback”,可能在帧结束时执行。我们要做的是在帧开始时触发视觉变化而不要错过它。

  • 将长时间运行的 JavaScript 计算交给 Web Workers 处理,就像我们之前讨论的一样。

  • 使用微任务进行多帧的 DOM 更新。这是为了防止任务需要访问 DOM,而 Web Workers 无法访问 DOM。这基本上意味着你可以将一个大任务分解成更小的任务,并根据任务的性质在 requestAnimationFramesetTimeoutsetInterval 中运行它们。

优化 CSS

通过添加和删除元素,修改属性等更改 DOM 将使浏览器重新计算元素样式,在许多情况下,还会重新计算整个页面或至少部分页面的布局。

要优化渲染,请考虑以下内容:

  • 减少选择器的复杂性。与构建样式本身的其他工作相比,选择器复杂性能占用计算元素样式所需的50%以上的时间。

  • 减少必须对其进行样式计算的元素的数量。大体上就是直接对一些元素进行样式更改,而不是使整个页面失效。

优化布局

布局的重新计算对于浏览器来说可能非常繁重。可以考虑以下方面的优化:

  • 尽可能减少布局的数量。更改样式时,浏览器将检查是否有任何更改需要重新计算布局。对 width,height,left,top 等属性以及通常与几何图形相关的属性的更改,都需要布局。所以,尽量避免改变这些属性。

  • 尽量在旧的布局模型上使用 flexbox。它渲染得更快,可以为你的应用程序创造巨大的性能优势。

  • 避免进行强制的同步布局。需要记住的是,当 JavaScript 运行时,来自前一帧的所有旧的布局值都是已知且能获取的。所以,如果你获取 box.offthesight 是没有问题的。但是,如果在获取前更改了盒子的样式(例如,给元素动态添加一些 CSS 类),浏览器必须先去更改样式,然后再进行布局。这可能非常耗费时间和资源,因此要尽可能避免。

优化绘制

绘制通常是所有任务中运行时间最长的,因此尽可能避免绘制是非常重要的。我们可以这样做:

  • 更改除了 transforms 或 opacity 以外的任何属性都会触发绘制。请谨慎使用。

  • 只要触发布局就会触发绘制,因为更改几何结构会导致元素的视觉的更改。

  • 通过图层优化和动画编排减少绘制区域

渲染是 sessionstack 正常运转的一个重要方面。SessionStack 必须将用户在浏览 Web 应用程序时遇到问题时发生的所有事情重新创建成一个视频。为此,sessionstack 仅利用我们的库收集的数据:用户事件、DOM更改、网络请求、异常、调试消息等。我们的播放器经过高度优化,能够正确地渲染和使用所有收集的数据,以便无论从视觉上还是技术上为用户的浏览器和浏览器中发生的一切提供一个完美像素模拟。

如果你想 试一试sessionstack,有一个免费的计划。

引用资源