JavaScript工作原理(十):渲染引擎和优化性能的技巧

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

但是,当您构建Web应用程序时,您不只是编写独立运行的独立JavaScript代码。您编写的JavaScript与环境进行交互。了解这种环境,它是如何工作的以及它的组成是什么,将使您能够构建更好的应用程序,并对应用程序发布后可能出现的潜在问题做好充分准备。
JavaScript工作原理(十):渲染引擎和优化性能的技巧

那么,让我们看看浏览器的主要组件是什么:

  • 用户界面:这包括地址栏,后退和前进按钮,书签菜单等。实质上,这是浏览器显示的每个部分,除了您看到网页本身的窗口。
  • 浏览器引擎:它处理用户界面和渲染引擎之间的交互
  • 渲染引擎:它负责显示网页。渲染引擎解析HTML和CSS,并在屏幕上显示解析的内容。
  • 网络:这些是网络调用,例如XHR请求,通过对不同平台使用不同的实现来实现,这些平台位于独立于平台的接口之后。在本系列的前一篇文章中,我们更详细地讨论了网络层。
  • UI后端:用于绘制核心小部件,如复选框和窗口。这个后端公开了一个不是平台特定的通用接口。它使用下面的操作系统UI方法。
  • JavaScript引擎:我们在该系列的前一篇文章中详细介绍了这一点。基本上,这是JavaScript执行的地方。
  • 数据持久性:您的应用可能需要在本地存储所有数据。支持的存储机制类型包括localStorage,indexDB,WebSQL和FileSystem。

在这篇文章中,我们将关注渲染引擎,因为它处理HTML和CSS的解析和可视化,这是大多数JavaScript应用程序不断与之交互的东西。

渲染引擎的概述

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

渲染引擎可以显示HTML和XML文档和图像。如果您使用额外的插件,引擎还可以显示不同类型的文档,如PDF。

渲染引擎

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

  • Gecko - 火狐
  • WebKit - Safari
  • Blink - Chrome,Opera(从15版开始)

渲染的过程

渲染引擎从网络层接收所请求文档的内容。
JavaScript工作原理(十):渲染引擎和优化性能的技巧

构建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树如下所示:
JavaScript工作原理(十):渲染引擎和优化性能的技巧
基本上,每个元素都被表示为所有其子元素的父节点子元素直接包含在它的内部。

构建CSSOM树

CSSOM指的是CSS对象模型。当浏览器构建页面的DOM时,它在引用外部theme.css CSS样式表的head部分遇到link标记。预计它可能需要该资源来呈现页面,它立即发出请求。假设theme.css文件包含以下内容:

body { 
  font-size: 16px;
}

p { 
  font-weight: bold; 
}

span { 
  color: red; 
}

p span { 
  display: none; 
}

img { 
  float: right; 
}

与HTML一样,引擎需要将CSS转换为浏览器可以使用的东西 - CSSOM。 以下是CSSOM树的外观:
JavaScript工作原理(十):渲染引擎和优化性能的技巧

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

让我们来看看我们给出的具体例子。包含在body元素中的span标签中的任何文本的字体大小为16像素,并且具有红色。这些样式是从body元素继承而来的。如果span元素是p元素的子元素,则由于正在应用更具体的样式,因此不会显示其内容。

另外请注意,上面的树不是完整的CSSOM树,只显示了我们决定在样式表中重写的样式。每个浏览器都提供了一组默认的样式,也称为“user agent styles” - 这是我们在没有明确提供任何样式时看到的。我们的样式简单地覆盖这些默认值。

构建渲染树

HTML中的可视指令与CSSOM树中的样式数据结合在一起用于创建渲染树。

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

Webkit中,渲染树中的每个节点都被称为的渲染器或渲染对象。

这就是上述DOM和CSSOM树的渲染器树的外观:
JavaScript工作原理(十):渲染引擎和优化性能的技巧

为了构建渲染树,浏览器大致如下:

  • 从DOM树的根开始,它遍历每个可见节点。某些节点不可见(例如,脚本标记,元标记等),并且由于它们未反映在呈现的输出中而被忽略。一些节点通过CSS隐藏,并且也从渲染树中省略。例如,span节点 - 在上面的例子中,它并不存在于渲染树中,因为我们有一个明确的规则来设置display:none属性。
  • 对于每个可见节点,浏览器找到适当的CSSOM规则并应用它们。
  • 它发出带有内容及其计算样式的可见节点

你可以在这里看看RenderObject的源代码(在WebKit中):https://github.com/WebKit/web...

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

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()方法以在屏幕上显示内容。

绘画可以是全局或增量式(与布局类似):

  • 全局 - 整个树被重新绘制。
  • 增量 - 只有一些渲染器以不影响整个树的方式进行更改。渲染器使其矩形在屏幕上无效。这会导致操作系统将其视为需要重绘和生成绘画事件的区域。操作系统通过将几个区域合并为一个智能方式来完成。

一般来说,了解绘画是一个渐进的过程是很重要的。为了更好的用户体验,渲染引擎会尝试尽快在屏幕上显示内容。它不会等到所有的HTML被解析,才开始构建和布置渲染树。内容的部分内容将被解析并显示,而该过程继续保持来自网络的其余内容项目。

处理脚本和样式表的顺序

当解析器到达<script>标记时,脚本将被立即解析并执行。文档解析暂停,直到脚本执行完毕。这意味着该过程是同步的。

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

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

优化渲染性能

如果您想优化您的应用,那么您需要关注五个主要方面。这些是您可以控制的区域:

  • JavaScript - 在之前的文章中,我们介绍了编写优化代码的主题,这些代码不会阻止UI,内存效率高等等。当涉及渲染时,我们需要考虑JavaScript代码与DOM元素之间的交互方式这一页。JavaScript可以在UI中创建大量更改,尤其是在SPA中。
  • 样式计算 - 这是确定哪个CSS规则适用于基于匹配选择器的元素的过程。一旦定义了规则,就会应用这些规则,并计算每个元素的最终样式。
  • 布局 - 一旦浏览器知道哪些规则适用于元素,就可以开始计算后者占用的空间以及它在浏览器屏幕上的位置。 Web的布局模型定义了一个元素可能会影响其他元素。例如,<body>的宽度会影响其子元素的宽度等等。这一切都意味着布局过程是计算密集型的。该绘图是在多个层次完成的。
  • Paint - 这是实际像素被填充的位置。该过程包括绘制文本,颜色,图像,边框,阴影等 - 每个元素的每个视觉部分。
  • 合成 - 由于页面部分被划分为多个图层,因此需要按照正确的顺序将其绘制到屏幕上,以便页面呈现正确。这非常重要,特别是对于重叠元素。

优化您的JavaScript

JavaScript经常触发浏览器中的视觉变化。当建立一个SPA时更是如此。

以下是关于JavaScript可以优化哪些部分以改善渲染的一些提示:

  • 为视觉更新避免setTimeout或setInterval。这些将在框架中的某个点调用回调,最后可能会发生。我们想要做的就是在画面开始时触发视觉变化,不要错过它。
  • 如前所述,将长时间运行的JavaScript计算移至Web Workers。
  • 使用微任务在多个框架中引入DOM更改。这是为了防止任务需要访问DOM,Web Worker不能访问这些任务。这基本上意味着你将一个大任务分解成更小的任务,并根据任务的性质在requestAnimationFrame,setTimeout,setInterval中运行它们。

优化你的CSS

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

要优化渲染,请考虑以下几点:

  • 减少选择器的复杂性。选择器的复杂度可能比计算元素样式所需的时间多50%,而构建样式本身的其余工作则需要花费超过50%的时间。
  • 减少style计算必须发生的元素数量。实质上,直接对几个元素进行样式更改,而不是使整个页面失效。

优化布局

浏览器的布局重新计算可能非常繁重。考虑以下优化:

  • 尽可能减少布局的数量。当您更改样式时,浏览器会检查是否有任何更改要求重新计算布局。对属性(如宽度,高度,左侧,顶部以及通常与几何相关的属性)的更改需要布局。所以,尽量避免改变它们。
  • 尽可能在较早的布局模型上使用Flexbox。它运行速度更快,可为您的应用程序创造巨大的性能优势。
  • 避免强制同步布局。需要记住的是,在JavaScript运行时,前一帧中的所有旧布局值都是已知的,可供您查询。如果你访问box.offsetHeight它不会是一个问题。但是,如果您在访问该框之前更改了框的样式(例如,通过向元素动态添加一些CSS类),浏览器必须先应用样式更改并运行布局。这可能非常耗时且耗费资源,因此请尽可能避免。

优化paint

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

  • 更改除变换或不透明度以外的任何属性都会触发绘画。谨慎使用它。
  • 如果触发layout,则还会触发paint,因为更改几何图形会导致元素的视觉更改。
  • 通过图层提升和动画编排来减少绘画区域。

相关推荐