浏览器如何渲染页面 ?

浏览器如何渲染页面 ?

渲染主流程

Webkit 渲染流程

Mozilla 的 Gecko 渲染流程

构建 DOM 树

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="style.css" 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 的原始字节,并根据文件的指定编码(例如 UTF-8)将它们转换成各个字符,然后分析出各个 HTML 标签、各个标签对应的属性等。最后,根据标签之间的关系,构建 DOM 树。

构建 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:

CSS 字节转换成字符,接着转换成标签和节点,最后会构建 CSSOM (CSS Object Model) 树:

渲染树构建、布局和绘制

CSSOM 树和 DOM 树合并成渲染树,然后用于计算每个可见元素的布局,并输出给绘制流程,将像素渲染到屏幕上。

合成渲染树

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

  • 从 DOM 树的根节点开始遍历每个可见节点。某些节点不可见(例如脚本标记、元标记等),因为它们不会体现在渲染输出中,所以会被忽略。某些节点通过 CSS 隐藏,因此在渲染树中也会被忽略,例如,上例中的 span 节点就不会出现在渲染树中,因为有一个显式规则在该节点上设置了 display:none 属性。
  • 对于每个可见节点,为其找到适配的 CSSOM 规则并应用到节点上。
  • 将可见节点、内容和计算的样式传递到下一个阶段。

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

布局

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

<!DOCTYPE html>
<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>

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

绘制

我们知道了哪些节点可见、它们的计算样式以及几何信息,我们终于可以将这些信息传递给最后一个阶段:将渲染树中的每个节点转换成屏幕上的实际像素。这一步通常称为 “绘制”“栅格化”

提升渲染性能

使用媒体查询避免 CSS 阻塞渲染

CSS 是阻塞渲染的资源。需要将它尽早、尽快地下载到客户端,以便缩短首次渲染的时间。HTML 其实也是阻塞渲染的资源,不过没有 HTML 就没有办法构建 DOM 树,所以 HTML 必须提供。

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

我们可以通过 CSS “媒体类型” 和 “媒体查询” 来解决这类用例:

<link href="style.css" rel="stylesheet">
<link href="print.css" rel="stylesheet" media="print">
<link href="other.css" rel="stylesheet" media="(min-width: 40em)">

上述媒体查询定义,

  • 第一行的 style.css,未提供任何媒体类型或查询,因此它适用于所有情况,也就是说,它始终会阻塞渲染。
  • 第二行的 print.css,只在打印内容的时候适用,因此在网页首次加载时,该样式表不需要阻塞渲染。
  • 最后一行的 other.css,只在符合条件时,浏览器才会阻塞渲染。

优化 JavaScript 脚本

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

  • 脚本在文档中的位置很重要。
  • 当浏览器遇到一个 script 标记时,DOM 构建将暂停,直至脚本完成执行,也就延缓了首次渲染
  • 如果浏览器尚未完成 CSSOM 的下载和构建,而我们却想在此时运行脚本,浏览器将延迟脚本执行和 DOM 构建,直至其完成 CSSOM 的下载和构建。

默认情况下,所有 JavaScript 都会阻止解析器。由于浏览器不了解脚本计划在页面上执行什么操作,它会作最坏的假设并阻止解析器。为此,我们可以将脚本标记为异步

<script src="app.js" async></script>

分析加载时间

使用 Navigation Timing API 和页面加载时发出的其他浏览器事件,您可以捕获并记录任何页面的真实 CRP (Critical Rendering Path) 性能。

  • domInteractive 表示 DOM 准备就绪的时间点。
  • domContentLoaded 一般表示 DOM 和 CSSOM 均准备就绪的时间点。如果没有阻塞解析器的 JavaScript,则 DOMContentLoaded 将在 domInteractive 后立即触发。
  • domComplete 表示网页及其所有子资源都准备就绪的时间点。
<!DOCTYPE html>
<html>
  <head>
    <title>Critical Path: Measure</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="style.css" rel="stylesheet">
    <script>
      function measureCRP() {
        var t = window.performance.timing,
          interactive = t.domInteractive - t.domLoading,
          dcl = t.domContentLoadedEventStart - t.domLoading,
          complete = t.domComplete - t.domLoading;
        var stats = document.createElement('p');
        stats.textContent = 'interactive: ' + interactive + 'ms, ' +
            'dcl: ' + dcl + 'ms, complete: ' + complete + 'ms';
        document.body.appendChild(stats);
      }
    </script>
  </head>
  <body onload="measureCRP()">
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg"></div>
  </body>
</html>

参考