浏览器的渲染机制

浏览器组成

浏览器主要由7个部分组成:

  • 用户界面(User Interface):定义了一些常用的浏览器组件,比如地址栏,返回、书签等等
  • 数据持久化(Data Persistence):指浏览器的 cookie、localStorage 等组件
  • 浏览器引擎(Browser engine):平台应用的相关接口,在用户界面和呈现引擎之间传送指令。
  • 渲染引擎(Rendering engine):处理 HTML、CSS 的解析与渲染
  • JavaScript解释器(JavaScript Interpreter):解析和执行 JavaScript 代码
  • 用户界面后端(UI Backend):指浏览器的的图形库等
  • 网络(Networking):用于网络调用,比如 HTTP 请求

浏览器内核

浏览器内核分为两部分:渲染引擎 (layout engineer 或 Rendering Engine) 和 JavaScript 引擎

渲染引擎:负责取得网页的内容(HTML、XML、图像等等)、整理讯息(例如加入 CSS 等),以及计算网页的显示方式,然后会输出至显示器或打印机

JavaScript 引擎:负责解析和执行 JavaScript 来实现网页的动态效果 浏览器的内核的不同对于网页的语法解释会有不同,所以渲染的效果也不相同。所有网页浏览器、电子邮件客户端以及其它需要编辑、显示网络内容的应用程序都需要内核,最开始渲染引擎和 JavaScript 引擎并没有区分的很明确,后来 JavaScript 引擎越来越独立,内核就倾向于只指渲染引擎

常见的浏览器内核:Trident(IE)、Gecko(火狐)、Blink(Chrome、Opera)、Webkit(Safari)

渲染引擎简介

Firefox 使用的是 Gecko,这是 Mozilla 公司“自制”的渲染引擎。而 Safari 和 Chrome(28版本以前) 浏览器使用的都是 Webkit。

2013年7月10日发布的 Chrome 28 版本中,Chrome浏览器开始正式使用 Blink 内核。所以,Webkit 已经成为了Chrome浏览器的前内核。

浏览器渲染页面的过程

从耗时的角度,浏览器请求、加载、渲染一个页面,时间花在下面五件事情上:

  1. DNS 查询
  2. TCP 连接
  3. HTTP 请求即响应
  4. 服务器响应
  5. 客户端渲染:浏览器对内容的渲染

浏览器渲染机制

  1. 处理 HTML 标记并构建 DOM 树。
  2. 处理 CSS 标记并构建 CSSOM 树。
  3. 将 DOM 与 CSSOM 合并成一个渲染树。
  4. 根据渲染树来布局,以计算每个节点的几何信息。
  5. 将各个节点绘制到屏幕上。

解析 HTML 成 DOM 树

这个解析过程大概可以分为几个步骤:

字节(Byte)→ 字符串(Characters)→ Tokens →节点(Nodes)→ DOM

第一步:浏览器从磁盘或网络读取 HTML 的原始字节,也就是传输的 0 和 1这样的字节数据,并根据文件的指定编码(例如 UTF-8)将它们转换成字符串。

第二步:将字符串转换成 Token。例如:“”、“” 等。Token 中会标识出当前 Token 是“开始标签”或是“结束标签”亦或是“文本”等信息。

第三步:在每个 Token 被生成后,会立刻消耗这个 Token 创建出节点对象,因此在构建 DOM 的过程中,不是等待所有的 Token 都生成后才去构建 DOM,而是一边生成 Token 一边消耗来生成节点对象。

注意:带有结束标签标识的 Token 不会创建节点对象

第四步:通过“开始标签”与“结束标签”来识别并关联节点之间的关系。当所有 Token 都生成并消耗完毕后,我们就得到了一颗完整的 DOM 树。

构建 CSSOM

解析 CSS 构建 CSSOM 的过程和构建 DOM 的过程非常的相似。当浏览器接收到一段 CSS,浏览器首先要做的是识别出 Token,然后构建节点并生成 CSSOM

字节(Byte)→ 字符串(Characters)→ Tokens →节点(Nodes)→ CSSOM

节点中样式可以通过继承得到,也可以自己设置,因此在构建的过程中浏览器得递归 CSSOM 树,然后确定具体的元素到底是什么样式。

为了 CSSOM 的完整性,也只有等构建完毕才能进入到下一个阶段,哪怕 DOM 已经构建完,它也得等 CSSOM,然后才能进入下一个阶段。

CSS 匹配 HTML 元素是一个相当复杂和有性能问题的事情

所以,DOM 树要小,CSS 尽量用 id 和 class,千万不要过渡层叠下去。所以,CSS 的加载速度与构建 CSSOM 的速度将直接影响首屏渲染速度,因此在默认情况下 CSS 被视为阻塞渲染的资源

构建渲染树

当我们生成 DOM 树和 CSSOM 树后,我们需要将这两颗树合并成渲染树,在构建渲染树的过程中浏览器需要做如下工作:

  • 从 DOM 树的根节点开始遍历每个可见节点。
  • 有些节点不可见(例如脚本 Token、元 Token等),因为它们不会体现在渲染输出中,所以会被忽略。
  • 某些节点被 CSS 隐藏,因此在渲染树中也会被忽略。例如某些节点设置了 display: none 属性。
  • 对于每个可见节点,为其找到适配的 CSSOM 规则并应用它们

渲染阻塞

在渲染的过程中,遇到一个 script 标记时,就会停止渲染,去请求脚本文件并执行脚本文件,因为浏览器渲染和 JavaScript 执行共用一个线程,而且这里必须是单线程操作,多线程会产生渲染 DOM 冲突。

JavaScript 的加载、解析与执行会严重阻塞 DOM 的构建。只有等到脚本文件执行完毕,才会去继续构建 DOM。

JavaScript 不单会阻塞 DOM 构建,还会导致 CSSOM 也阻塞 DOM 的构建,如果 JavaScript 脚本还操作了CSSOM,而正好这个 CSSOM 还没有下载和构建,浏览器甚至会延迟脚本执行和构建 DOM,直至完成其 CSSOM的下载和构建,然后再执行 JavaScript,最后在继续构建 DOM。

因此 script 的位置很重要,在实际使用过程中遵循以下两个原则:

  • CSS 优先:引入顺序上,CSS 资源先于 JavaScript 资源。
  • JavaScript 置后:我们通常把 JS代码放到页面底部,且 JavaScript 应尽量少影响 DOM 的构建。

布局与绘制

浏览器拿到渲染树后,就会从渲染树的根节点开始遍历,然后确定每个节点对象在页面上的确切大小与位置,通常这一行为也被称为“自动重排”。

布局阶段的输出是一个盒子模型,它会精确地捕获每个元素在屏幕内的确切位置与大小,所有相对测量值都将转换为屏幕上的绝对像素。这一过程也可称为回流

布局完成后,浏览器会立即发出“Paint Setup”和“Paint”事件,将渲染树转换成屏幕上的像素。

性能优化策略

回流(reflow) 与 重绘(repaint)

当元素的样式发生变化时,浏览器需要触发更新,重新绘制元素。这个过程中,有两种类型的操作,即重绘与回流。

  • 重绘(repaint): 当元素样式的改变不影响布局时,浏览器将使用重绘对元素进行更新,此时由于只需要UI层面的重新像素绘制,因此损耗较少

  • 回流(reflow): 当元素的尺寸、结构或触发某些属性时,浏览器会重新渲染页面,称为回流。此时,浏览器需要重新经过计算,计算后还需要重新页面布局,因此是较重的操作。

会触发回流的操作:

  • 添加或删除可见的 DOM 元素
  • 元素的位置发生变化
  • 元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)
  • 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代。
  • 页面一开始渲染的时候(这肯定避免不了)
  • 浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的
  • 注意:回流一定会触发重绘,而重绘不一定会回流,重绘的开销较小,回流的代价较高

因此为了减少性能优化,我们可以尽量避免回流或者重绘操作 CSS

  • 避免使用table布局

  • 将动画效果应用到 position 属性为 absolute 或 fixed 的元素上

JavaScript

  • 避免频繁操作样式,可汇总后统一 一次修改
  • 尽量使用class进行样式修改
  • 减少dom的增删次数,可使用 字符串 或者 documentFragment 一次性插入
  • 极限优化时,修改样式可将其 display: none 后修改
  • 避免多次触发上面提到的那些会触发回流的方法,可以的话尽量用变量存住

探讨 requestAnimationFrame

在 JavaScript 中,我们可以使用 setTimeoutsetIntarval 实现动画,但是 H5 的出现,让我们又多了两种实现动画的方式,分别是 CSS 动画(transitionanimation)和 H5 的canvas 实现。

由于 JavaScript 是单线程的,所以定时器的实现是在当前任务队列完成后再执行定时器的回调的,假如当前队列任务执行时间大于定时器设置的延迟时间,那么定时器就不是那么可靠了。

所以,H5 还提供了一个专门用于请求动画的 API,让 DOM 动画、canvas 动画、svg 动画、webGL 动画等有一个专门的刷新机制。

是什么?

动画是由浏览器按照一定的频率一帧一帧的绘制的,由 CSS 实现的动画的优势就是浏览器知道动画的开始及每一帧的循环间隔,能够在恰当的时间刷新 UI,给用户一种流畅的体验。

setIntervalsetTimeout实现的 JavaScript 动画就没有这么可靠了,因为浏览器压根就无法保证每一帧渲染的时间间隔。

一般情况下,每秒平均刷新次数能够达到 60帧,就能够给人流畅的体验,即每过 1000/60 毫秒渲染新一帧即可,这一点单靠定时器是无法保证的。 为此,requestAnimationFrame 应运而生,其作用就是让浏览器流畅的执行动画效果。

可以将其理解为专门用来实现动画效果的 API,通过这个 API 可以告诉浏览器某个 JavaScript 代码要执行动画,浏览器收到通知后,则会运行这些代码的时候进行优化,实现流畅的效果,而不再需要开发人员烦心刷新频率的问题了。

requestAnimationFrame 方法会告诉浏览器希望执行动画并请求浏览器在下一次重绘之前调用回调函数来更新动画。

window.requestAnimationFrame(callback)
  • callback:下一次重绘之前更新动画帧所调用的函数,callback仅有一个参数,为DOMHighResTimeStamp参数,表示requestAnimationFrame()开始执行回调函数的时刻。
  • 返回值:一个 long 类型整数,唯一标志元组在列表中的位置,你可以传这个值给cancelAnimationFrame() 以取消动画。

在使用和实现上, requestAnimationFramesetTimeout 类似。举个例子:

let count = 0;
let rafId = null;
/**
 * 回调函数
 * @param time requestAnimationFrame 调用该函数时,自动传入的一个时间
 */
function requestAnimation(time) {
  console.log(time); // 打印执行requestAnimation函数的时刻
  // 动画没有执行完,则递归渲染
  if (count < 5) {
    count++;
    // 渲染下一帧
    rafId = window.requestAnimationFrame(requestAnimation);
  }
}
// 渲染第一帧
window.requestAnimationFrame(requestAnimation);

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!