浏览器渲染原理


作者:Seiya

时间:2019年09月02日


浏览器工作大致流程


  • 首先,浏览器会解析三个东西:

    • HTML/SVG/XHTML

      事实上,Webkit有三个C++的类对应这三类文档。解析这三种文件会产生一个DOM Tree。

    • CSS

      解析CSS会产生CSS规则树。

    • Javascript

      主要是通过 DOM API 和 CSSOM API 来操作 DOM Tree 和 CSS Rule Tree。


  • 解析完成后,浏览器引擎会通过 DOM Tree 和 CSS Rule Tree 来构造 Rendering Tree。

    Rendering Tree 渲染树并不等同于 DOM 树,因为一些像 Header 或 display:none 的东西就没必要放在渲染树中了。 CSS 的 Rule Tree 主要是为了完成匹配并把 CSS Rule 附加上 Rendering Tree 上的每个 Element。也就是 DOM 结点。也就是所谓的 Frame。 然后,计算每个 Frame(也就是每个 Element)的位置,这又叫 layout 和 reflow 过程。


  • 最后通过调用操作系统 Native GUI 的 API 绘制。




HTML 解析


浏览器渲染引擎从网络层取得请求的文档,一般情况下文档会分成8kB大小的分块传输。HTML 解析器的主要工作是对 HTML 文档进行解析,生成解析树。

解析树是以 DOM 元素以及属性为节点的树。DOM 是文档对象模型(Document Object Model)的缩写,它是 HTML 文档的对象表示,同时也是 HTML 元素面向外部(如Javascript)的接口。树的根部是"Document"对象。整个 DOM 和 HTML 文档几乎是一对一的关系。



解析算法

HTML 不能使用常见的自顶向下或自底向上方法来进行分析。主要原因有以下几点:

  • 语言本身的“宽容”特性;

  • HTML 本身可能是残缺的,对于常见的残缺,浏览器需要有传统的容错机制来支持它们;

  • 解析过程需要反复;

    对于其他语言来说,源码不会在解析过程中发生变化,但是对于 HTML 来说,动态代码,例如脚本元素中包含的 document.write() 方法会在源码中添加内容,也就是说,解析过程实际上会改变输入的内容;

由于不能使用常用的解析技术,浏览器创造了专门用于解析 HTML 的解析器。解析算法在 HTML5 标准规范中有详细介绍,算法主要包含了两个阶段:标记化(tokenization)和树的构建。


<html>
  <head>
    <title>Web page parsing</title>
  </head>
  <body>
    <div>
      <h1>Web page parsing</h1>
      <p>This is an example Web page.</p>
    </div>
  </body>
</html>

上面这段HTML会解析成这样:



解析结束

当 HTML 解析结束,浏览器开始加载网页的外部资源(CSS,图像,Javascript 文件等)。

此时浏览器把文档标记为可交互的(interactive),浏览器开始解析处于“推迟(deferred)”模式的脚本,也就是那些需要在文档解析完毕之后再执行的脚本。之后文档的状态会变为“完成(complete)”,浏览器会触发“加载(load)”事件。


tips

解析 HTML 网页时永远不会出现“无效语法(Invalid Syntax)”错误,浏览器会修复所有错误内容,然后继续解析。





CSS 解析


在这一过程中,浏览器会确定下每一个节点的样式到底是什么,并且这一过程其实是很消耗资源的。因为样式你可以自行设置给某个节点,也可以通过继承获得。在这一过程中,浏览器得递归 CSSOM 树,然后确定具体的元素到底是什么样式。

  • 根据 CSS 词法和句法,分析 CSS 文件和 <style> 标签包含的内容以及 style 属性的值;

  • 每个 CSS 文件都被解析成一个样式表对象(StyleSheet object),这个对象里包含了带有选择器的 CSS 规则,和对应 CSS 语法的对象;

  • CSS 解析器可能是自顶向下的,也可能是使用解析器生成器生成的自底向上的解析器;





页面渲染


当我们生成 DOM 树和 CSSOM 树以后,就需要将这两棵树组合为渲染树。在这一过程中,不是简单的将两者合并就行了。渲染树只会包括需要显示的节点和这些节点的样式信息,如果某个节点是 display: none 的,那么就不会在渲染树中显示。

当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做回流),然后调用 GPU 绘制,合成图层,显示在屏幕上。对于这一部分的内容因为过于底层,还涉及到了硬件相关的知识,这里就不再继续展开内容了。



页面渲染流程

  • 通过遍历 DOM 节点树创建一个“Frame 树”或“渲染树”,并计算每个节点的各个 CSS 样式值;

    • 通过累加子节点的宽度,该节点的水平内边距(padding)、边框(border)和外边距(margin),自底向上的计算"Frame 树"中每个节点的首选(preferred)宽度;

    • 通过自顶向下的给每个节点的子节点分配可行宽度,计算每个节点的实际宽度;

    • 通过应用文字折行、累加子节点的高度和此节点的内边距(padding)、边框(border)和外边距(margin),自底向上的计算每个节点的高度;

    • 使用上面的计算结果构建每个节点的坐标;

    • 当存在元素使用 floated,位置有 absolutely 或 relatively 属性的时候,会有更多复杂的计算

  • 创建 layer (层)来表示页面中的哪些部分可以成组的被绘制,而不用被重新栅格化处理。每个帧对象都被分配给一个层;

  • 页面上的每个层都被分配了纹理;

  • 每个层的帧对象都会被遍历,计算机执行绘图命令绘制各个层,此过程可能由 CPU 执行栅格化处理,或者直接通过 D2D/SkiaGL 在 GPU 上绘制;

  • 计算出各个层的最终位置,一组命令由 Direct3D/OpenGL发出,GPU 命令缓冲区清空,命令传至 GPU 并异步渲染,帧被送到 Window Server。



GPU 渲染

  • 在渲染过程中,图形处理层可能使用通用用途的 CPU,也可能使用图形处理器 GPU;

  • 当使用 GPU 用于图形渲染时,图形驱动软件会把任务分成多个部分,这样可以充分利用 GPU 强大的并行计算能力,用于在渲染过程中进行大量的浮点计算;





其它知识点


为什么操作 DOM 慢

因为 DOM 是属于渲染引擎中的东西,而 JS 又是 JS 引擎中的东西。当我们通过 JS 操作 DOM 的时候,其实这个操作涉及到了两个线程之间的通信,那么势必会带来一些性能上的损耗。操作 DOM 次数一多,也就等同于一直在进行线程之间的通信,并且操作 DOM 可能还会带来重绘回流的情况,所以也就导致了性能上的问题。



插入几万个 DOM,如何实现页面不卡顿

  • 方式一:通过 requestAnimationFrame 的方式去循环的插入 DOM;

  • 方式二:虚拟滚动

    只渲染可视区域内的内容,非可见区域的那就完全不渲染了,当用户在滚动的时候就实时去替换渲染的内容。



什么情况阻塞渲染

首先,渲染的前提是生成渲染树,所以 HTML 和 CSS 肯定会阻塞渲染。如果你想渲染的越快,你越应该降低一开始需要渲染的文件大小,并且扁平层级,优化选择器。

然后,当浏览器在解析到 script 标签时,会暂停构建 DOM,完成后才会从暂停的地方重新开始。也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件,这也是都建议将 script 标签放在 body 标签底部的原因。当然,你也可以给 script 标签添加 defer 或者 async 属性。

当 script 标签加上 defer 属性以后,表示该 JS 文件会并行下载,但是会放到 HTML 解析完成后顺序执行,所以对于这种情况你可以把 script 标签放在任意位置。对于没有任何依赖的 JS 文件可以加上 async 属性,表示 JS 文件下载和解析不会阻塞渲染。



重绘(Repaint)和回流(Reflow)

重绘和回流会在设置节点样式时频繁出现,同时也会很大程度上影响性能。

  • 重绘是当节点需要更改外观而不会影响布局的,比如改变 color 就叫称为重绘;

  • 回流是布局或者几何属性需要改变就称为回流;

回流必定会发生重绘,重绘不一定会引发回流。回流所需的成本比重绘高的多,改变父节点里的子节点很可能会导致父节点的一系列回流。


以下几个动作可能会导致性能问题:

  • 改变 window 大小

  • 改变字体

  • 添加或删除样式

  • 文字改变

  • 定位或者浮动

  • 盒模型


重绘和回流其实也和 Eventloop 有关:

  • 当 Eventloop 执行完 Microtasks 后,会判断 document 是否需要更新,因为浏览器是 60Hz 的刷新率,每 16.6ms 才会更新一次。

  • 然后判断是否有 resize 或者 scroll 事件,有的话会去触发事件,所以 resize 和 scroll 事件也是至少 16ms 才会触发一次,并且自带节流功能。

  • 判断是否触发了 media query

  • 更新动画并且发送事件

  • 判断是否有全屏操作事件

  • 执行 requestAnimationFrame 回调

  • 执行 IntersectionObserver 回调,该方法用于判断元素是否可见,可以用于懒加载上,但是兼容性不好

  • 更新界面

  • 以上就是一帧中可能会做的事情。如果在一帧中有空闲时间,就会去执行 requestIdleCallback 回调。



减少重绘和回流

  • 使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局);

  • 不要把节点的属性值放在一个循环里当成循环里的变量;

  • 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局;

  • 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame;

  • CSS 选择符从右往左匹配查找,避免节点层级过多;

  • 将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点。比如对于 video 标签来说,浏览器会自动将该节点变为图层。



在不考虑缓存和优化网络协议的前提下,考虑可以通过哪些方式来最快的渲染页面

当发生 DOMContentLoaded 事件后,就会生成渲染树,生成渲染树就可以进行渲染了,这一过程更大程度上和硬件有关系了。

  • 从文件大小考虑

  • 从 script 标签使用上来考虑

  • 从 CSS、HTML 的代码书写上来考虑

  • 从需要下载的内容是否需要在首屏使用上来考虑

最后更新时间: 2019-9-3 17:02:48