首页 >web前端 >js教程 >总结浏览器渲染页面的方法

总结浏览器渲染页面的方法

零下一度
零下一度原创
2017-06-26 10:24:092886浏览

转载自web fundamental

构建对象模型

浏览器渲染页面前需要先构建 DOM 和 CSSOM 树。因此,我们需要确保尽快将 HTML 和 CSS 都提供给浏览器。

  • 字节 → 字符 → 标记 → 节点 → 对象模型。

  • HTML 标记转换成文档对象模型 (DOM);CSS 标记转换成 CSS 对象模型 (CSSOM)。DOM 和 CSSOM 是独立的数据结构。

  • Chrome DevTools Timeline可以捕获和检查 DOM 和 CSSOM 的构建和处理开销。

文档对象模型 (DOM)

<html>
  <head><meta name="viewport" content="width=device-width,initial-scale=1"><link href="style.css?1.1.11" rel="stylesheet"><title>Critical Path</title>
  </head>
  <body><p>Hello <span>web performance</span> students!</p><div><img src="awesome-photo.jpg"></div>
  </body></html>

一个包含一些文本和一幅图片的普通 HTML 页面,浏览器如何处理此页面?

HTML解析器输出的树是由DOM元素和属性节点组成的,它是HTML文档的对象化描述,也是HTML元素与外界(如Javascript)的接口。DOM与标签有着几乎一一对应的关系。

  

  1. 转换: 浏览器从磁盘或网络读取 HTML 的原始字节,并根据文件的指定编码(如 UTF-8)将它们转换成各个字符。

  2. Tokenizing 浏览器将字符串转换成 W3C HTML5 标准规定的各种tokens,例如,“”、“”,以及其他尖括号内的字符串。每个token都具有特殊含义和一组规则。

  3. 词法分析: 发出的标记转换成定义其属性和规则的“对象”。

  4. DOM 构建: 最后,由于 HTML 标记定义不同标记之间的关系(一些标记包含在其他标记内),创建的对象链接在一个树数据结构内,此结构也会捕获原始标记中定义的父项-子项关系:HTML 对象是 body 对象的父项,bodyparagraph对象的父项,依此类推。

整个流程最终输出是页面的文档对象模型 (DOM),浏览器对页面进行的所有进一步处理都会用到它。

浏览器每次处理 HTML 标记时,都会完成以上所有步骤:将字节转换成字符,确定tokens,将tokens转换成节点,然后构建 DOM 树。这整个流程可能需要一些时间才能完成,有大量 HTML 需要处理时更是如此。

 

如果您打开 Chrome DevTools 并在页面加载时记录时间线,就可以看到执行该步骤实际花费的时间。在上例中,将一堆 HTML 字节转换成 DOM 树大约需要 5 毫秒。对于较大的页面,这一过程需要的时间可能会显著增加。创建流畅动画时,如果浏览器需要处理大量 HTML,这很容易成为瓶颈。

DOM 树捕获文档标记的属性和关系,但并未告诉我们元素在渲染后呈现的外观。那是 CSSOM 的责任。

CSS 对象模型 (CSSOM)

在浏览器构建这个简单页面的 DOM 过程中,在文档的 head 中遇到了一个 link 标记,该标记引用一个外部 CSS 样式表:style.css。由于预见到需要利用该资源来渲染页面,它会立即发出对该资源的请求,并返回以下内容:

body { font-size: 16px }p { font-weight: bold }span { color: red }p span { display: none }img { float: right }

我们本可以直接在 HTML 标记内声明样式(内联),但让 CSS 独立于 HTML 有利于我们将内容和设计作为独立关注点进行处理:设计人员负责处理 CSS,开发者侧重于 HTML,等等。

与处理 HTML 时一样,我们需要将收到的 CSS 规则转换成某种浏览器能够理解和处理的东西。因此,我们会重复 HTML 过程,不过是为 CSS 而不是 HTML:

 

CSS 字节转换成字符,接着转换成tokens和节点,最后链接到一个称为“CSS 对象模型”(CSSOM) 的树结构:

 

CSSOM 为何具有树结构?为页面上的任何节点对象计算最后一组样式时,浏览器都会先从适用于该节点的最通用规则开始(例如,如果该节点是 body 元素的子元素,则应用所有 body 样式),然后通过应用更具体的规则以递归方式优化计算的样式。

以上面的 CSSOM 树为例进行更具体的阐述。任何置于 body 元素内span 标记中的文本都将具有 16 像素字号,并且颜色为红色 。font-size 指令从 body 向下级层叠至 span。不过,如果某个 span 标记是某个段落 (p) 标记的子项,则其内容将不会显示。

Also, note that the above tree is not the complete CSSOM tree and only shows the styles we decided to override in our stylesheet.每个浏览器都提供一组默认样式(也称为“User Agent 样式”),即我们的样式只是override这些默认样式。

要了解 CSS 处理所需的时间,可以在 DevTools 中记录时间线并寻找“Recalculate Style”事件:unlike DOM parsing, the timeline doesn’t show a separate "Parse CSS" entry, and instead captures parsing and CSSOM tree construction, plus the recursive calculation of computed styles under this one event.

 

我们的小样式表需要大约 0.6 毫秒的处理时间,影响页面上的 8 个元素 — 虽然不多,但同样会产生开销。不过,这 8 个元素从何而来呢?将 DOM 与 CSSOM 关联在一起的是渲染树。

渲染树构建、布局及绘制

CSSOM 树和 DOM 树合并成渲染树,然后用于计算每个可见元素的布局,并输出给绘制流程,将像素渲染到屏幕上。优化上述每一个步骤对实现最佳渲染性能至关重要。

浏览器根据 HTML 和 CSS 输入构建了 DOM 树和 CSSOM 树。 不过,它们是彼此完全独立的对象,分别capture文档不同方面的信息:一个描述内容,另一个则是描述需要对文档应用的样式规则。我们该如何将两者合并,让浏览器在屏幕上渲染像素呢?

  • DOM 树与 CSSOM 树合并后形成渲染树,它只包含渲染网页所需的节点。遍历每个DOM树中的node节点,在CSSOM规则树中寻找当前节点的样式,生成渲染树。

  • 布局计算每个对象的精确位置和大小。

  • 最后一步是绘制,使用最终渲染树将像素渲染到屏幕上。

 

第一步是让浏览器将 DOM 和 CSSOM 合并成一个“渲染树”,网罗网页上所有可见的 DOM 内容,以及每个节点的所有 CSSOM 样式信息。

 

为构建渲染树,浏览器大体上完成了下列工作:

  1. 从 DOM 树的根节点开始遍历每个可见节点。

  • 某些节点不可见(例如脚本标记、元标记等),因为它们不会体现在渲染输出中,所以会被忽略。

  • 某些节点通过 CSS 隐藏,因此在渲染树中也会被忽略。例如 span 节点上设置了“display: none”属性,所以也不会出现在渲染树中。

  • 遍历每个可见节点,为其找到适配的 CSSOM 规则并应用它们。从选择器的右边往左边开始匹配,也就是从CSSOM树的子节点开始往父节点匹配。

  • Emit visible nodes with content and their computed styles.

  • 注: visibility: hidden 与 display: none 是不一样的。前者隐藏元素,但元素仍占据着布局空间(即将其渲染成一个空框),而后者 (display: none) 将元素从渲染树中完全移除,元素既不可见,也不是布局的组成部分。

    最终输出的渲染同时包含了屏幕上的所有可见内容及其样式信息。有了渲染树,我们就可以进入布局阶段。

    到目前为止,我们计算了哪些节点应该是可见的以及它们的计算样式,但我们尚未计算它们在设备视口内的确切位置和大小---这就是“布局”阶段,也称为“reflow”。

    为弄清每个对象在网页上的确切大小和位置,浏览器从渲染树的根节点开始进行遍历。让我们考虑一个简单的实例:

    <html>
      <head><meta name="viewport" content="width=device-width,initial-scale=1"><title>Critial Path: Hello world!</title>
      </head>
      <body><div style="width: 50%">  <div style="width: 50%">Hello world!</div></div>
      </body></html>

    以上网页的正文包含两个嵌套 div:第一个(父)div 将节点的显示尺寸设置为视口宽度的 50%,父 div 包含的第二个 div宽度为其父项的 50%,即视口宽度的 25%。

     

     

    布局流程的输出是一个“盒模型”,它会精确地捕获每个元素在视口内的确切位置和尺寸:所有相对测量值都转换为屏幕上的绝对像素。

    最后,既然我们知道了哪些节点可见、它们的computed styles以及几何信息,我们终于可以将这些信息传递给最后一个阶段:将渲染树中的每个节点转换成屏幕上的实际像素。这一步通常称为"painting" or "rasterizing."。

    Chrome DevTools 可以帮助我们对上述所有三个阶段的耗时进行深入的了解。让我们看一下最初“hello world”示例的布局阶段:

     

    The "Layout" event captures the render tree construction, position, and size calculation in the Timeline.

    When layout is complete, the browser issues "Paint Setup" and "Paint" events, which convert the render tree to pixels on the screen.

    执行渲染树构建、布局和绘制所需的时间将取决于文档大小、应用的样式,以及运行文档的设备:文档越大,浏览器需要完成的工作就越多;样式越复杂,绘制需要的时间就越长(例如,单色的绘制开销“较小”,而阴影的计算和渲染开销则要“大得多”)。

    下面简要概述了浏览器完成的步骤:

    1. 处理 HTML 标记并构建 DOM 树。

    2. 处理 CSS 标记并构建 CSSOM 树。

    3. 将 DOM 与 CSSOM 合并成一个渲染树。

    4. 根据渲染树来布局,以计算每个节点的几何信息。

    5. 将各个节点绘制到屏幕上。

    如果 DOM 或 CSSOM 被修改,需要再执行一遍以上所有步骤,以确定哪些像素需要在屏幕上进行重新渲染。

    Optimizing the critical rendering path is the process of minimizing the total amount of time spent performing steps 1 through 5 in the above sequence. Doing so renders content to the screen as quickly as possible and also reduces the amount of time between screen updates after the initial render; that is, achieve higher refresh rates for interactive content.

    阻塞渲染的 CSS

    默认情况下,CSS 被视为阻塞渲染的资源(但不阻塞html的解析),这意味着浏览器将不会渲染任何已处理的内容,直至 CSSOM 构建完毕请务必精简CSS,尽快提供它,并利用媒体类型和查询来解除对渲染的阻塞,以缩短首屏的时间。

    在渲染树构建中,要求同时具有 DOM 和 CSSOM 才能构建渲染树。这会给性能造成严重影响:HTML CSS 都是阻塞渲染的资源。 HTML 显然是必需的,因为如果没有 DOM,就没有可渲染的内容,但 CSS 的必要性可能就不太明显。如果在 CSS 不阻塞渲染的情况下尝试渲染一个普通网页会怎样?

    • 默认情况下,CSS 被视为阻塞渲染的资源。

    • 我们可以通过媒体类型和媒体查询将一些 CSS 资源标记为不阻塞渲染。

    • 浏览器会下载所有 CSS 资源,无论阻塞还是不阻塞。

    没有 CSS 的网页实际上无法使用。所以浏览器将阻塞渲染,直至 DOM 和 CSSOM 全都准备就绪。

    CSS 是阻塞渲染的资源。需要将它尽早、尽快地下载到客户端,以便缩短首次渲染的时间。

    如果有一些 CSS 样式只在特定条件下(例如显示网页或将网页投影到大型显示器上时)使用,又该如何?如果这些资源不阻塞渲染,该有多好。

    可以通过 CSS“媒体类型”和“媒体查询”来解决这类情况:



    媒体查询由媒体类型以及零个或多个检查特定媒体特征状况的表达式组成。例如,第一个样式表声明未提供任何媒体类型或查询,因此它适用于所有情况。也就是说它始终会阻塞渲染。第二个样式表则不然,它只在打印内容时适用---或许您想重新安排布局、更改字体等等,因此在网页首次加载时,该样式表不需要阻塞渲染。最后一个样式表声明提供了由浏览器执行的“媒体查询”:符合条件时,样式表会生效,浏览器将阻塞渲染,直至样式表下载并处理完毕。

    通过使用媒体查询,我们可以根据特定用例(比如显示或打印),也可以根据动态情况(比如屏幕方向变化、尺寸调整事件等)定制外观。声明样式表时,请密切注意媒体类型和查询,因为它们将严重影响关键渲染路径的性能。

    让我们考虑下面这些实例:





    • 第一个声明阻塞渲染,适用于所有情况。

    • 第二个声明同样阻塞渲染:“all”是默认类型,和第一个声明实际上是等效的。

    • 第三个声明具有动态媒体查询,将在网页加载时计算。根据网页加载时设备的方向,portrait.css 可能阻塞渲染,也可能不阻塞渲染。

    • 最后一个声明只在打印网页时应用,因此网页在浏览器中加载时,不会阻塞渲染。

    最后,“阻塞渲染”仅是指浏览器是否需要暂停网页的首次渲染,直至该资源准备就绪。无论媒寻是否命中,浏览器都会下载上述所有的CSS样式表,只不过不阻塞渲染的资源对当前媒体不生效罢了。

    使用 JavaScript 添加交互

    JavaScript 允许我们修改网页的方方面面:内容、样式以及它如何响应用户交互。不过,JavaScript 也会阻止 DOM 构建和延缓网页渲染。为了实现最佳性能,可以让 JavaScript 异步执行,并去除关键渲染路径中任何不必要的 JavaScript。

    • JavaScript 可以查询和修改 DOM 与 CSSOM。

    • JavaScript的 执行会阻止 CSSOM的构建,所以和CSSOM的构建是互斥的。

    • JavaScript blocks DOM construction unless explicitly declared as async.

    JavaScript 是一种运行在浏览器中的动态语言,它允许对网页行为的几乎每一个方面进行修改:可以通过在 DOM 树中添加和移除元素来修改内容;可以修改每个元素的 CSSOM 属性;可以处理用户输入等等。为进行说明,让我们用一个简单的内联脚本对之前的“Hello World”示例进行扩展:

    <html>
      <head><meta name="viewport" content="width=device-width,initial-scale=1"><link href="style.css?1.1.11" rel="stylesheet"><title>Critical Path: Script</title><style> body { font-size: 16px };p { font-weight: bold };
        span { color: red };p span { display: none };
        img { float: right }</style>
      </head>
      <body><p>Hello <span>web performance</span> students!</p><div><img src="awesome-photo.jpg"></div><script>  var span = document.getElementsByTagName('span')[0];
          span.textContent = 'interactive'; // change DOM text content      span.style.display = 'inline';  // change CSSOM property  // create a new element, style it, and append it to the DOM  var loadTime = document.createElement('div');
          loadTime.textContent = 'You loaded this page on: ' + new Date();
          loadTime.style.color = 'blue';
          document.body.appendChild(loadTime);</script>
      </body></html>
    • JavaScript 允许我们进入 DOM 并获取对隐藏的 span 节点的引用 -- 该节点可能未出现在渲染树中,却仍然存在于 DOM 内。然后,在获得引用后,就可以更改其文本,并将 display 样式属性从“none”替换为“inline”。现在,页面显示“Hello interactive students!”。

    • JavaScript 还允许我们在 DOM 中创建、样式化、追加和移除新元素。从技术上讲,整个页面可以是一个大的 JavaScript 文件,此文件逐一创建元素并对其进行样式化。但是在实践中,使用 HTML 和 CSS 要简单得多。

    尽管 JavaScript 为我们带来了许多功能,不过也在页面渲染方式和时间方面施加了更多限制。

    首先,请注意上例中的内联脚本靠近网页底部。为什么呢?如果我们将脚本移至 span元素前面,就会脚本运行失败,并提示在文档中找不到对任何span 元素的引用 -- 即 getElementsByTagName(‘span') 会返回 null。这透露出一个重要事实:脚本在文档的何处插入,就在何处执行。当 HTML 解析器遇到一个 script 标记时,它会暂停构建 DOM,将控制权移交给 JavaScript 引擎;等 JavaScript 引擎运行完毕,浏览器会从中断的地方恢复 DOM 构建。

    换言之,我们的脚本块在运行时找不到网页中任何靠后的元素,因为它们尚未被处理!或者说:执行内联脚本会阻止 DOM 构建,也就延缓了首次渲染。

    在网页中引入脚本的另一个微妙事实是,它们不仅可以读取和修改 DOM 属性,还可以读取和修改 CSSOM 属性。实际上,示例中就是这么做的:将 span 元素的 display 属性从 none 更改为 inline。最终结果如何?我们现在遇到了race condition(资源竞争)。

    如果浏览器尚未完成 CSSOM 的下载和构建,而却想在此时运行脚本,会怎样?答案很简单,对性能不利:浏览器将延迟脚本执行和 DOM 构建,直至其完成 CSSOM 的下载和构建。

    简言之,JavaScript 在 DOM、CSSOM 和 JavaScript 执行之间引入了大量新的依赖关系,从而可能导致浏览器在处理以及在屏幕上渲染网页时出现大幅延迟:

    • 脚本在文档中的位置很重要。

    • 当浏览器遇到一个 script 标记时,DOM 构建将暂停,直至脚本完成执行。

    • JavaScript 可以查询和修改 DOM 与 CSSOM。

    • JavaScript 执行将暂停,直至 CSSOM 就绪。即CSSDOM构建的优先级更高。

    “优化关键渲染路径”在很大程度上是指了解和优化 HTML、CSS 和 JavaScript 之间的依赖关系谱。

    解析器阻塞与异步 JavaScript

    默认情况下,JavaScript 执行会“阻塞解析器”:当浏览器遇到文档中的脚本时,它必须暂停 DOM 构建,将控制权移交给 JavaScript 运行时,让脚本执行完毕,然后再继续构建 DOM。实际上,内联脚本始终会阻止解析器,除非编写额外代码来推迟它们的执行。

    通过 script 标签引入的脚本又怎样:

    <html>
      <head><meta name="viewport" content="width=device-width,initial-scale=1"><link href="style.css?1.1.11" rel="stylesheet"><title>Critical Path: Script External</title>
      </head>
      <body><p>Hello <span>web performance</span> students!</p><div><img src="awesome-photo.jpg"></div><script src="app.js?1.1.11"></script>
      </body></html>

    app.js

    var span = document.getElementsByTagName('span')[0];
    span.textContent = 'interactive'; // change DOM text contentspan.style.display = 'inline';  // change CSSOM property// create a new element, style it, and append it to the DOMvar loadTime = document.createElement('div');
    loadTime.textContent = 'You loaded this page on: ' + new Date();
    loadTime.style.color = 'blue';
    document.body.appendChild(loadTime);

    无论我们使用