关于golang:深入浅出浏览器渲染原理

54次阅读

共计 6021 个字符,预计需要花费 16 分钟才能阅读完成。

前言

浏览器的内核是指反对浏览器运行的最外围的程序,分为两个局部的,一是渲染引擎,另一个是 JS 引擎。渲染引擎在不同的浏览器中也不是都雷同的。目前市面上常见的浏览器内核能够分为这四种:Trident(IE)、Gecko(火狐)、Blink(Chrome、Opera)、Webkit(Safari)。这外面大家最耳熟能详的可能就是 Webkit 内核了,Webkit 内核是当下浏览器世界真正的霸主。
本文咱们就以 Webkit 为例,对古代浏览器的渲染过程进行一个深度的分析。

页面加载过程

在介绍浏览器渲染过程之前,咱们简明扼要介绍下页面的加载过程,有助于更好了解后续渲染过程。

要点如下:

  • 浏览器依据 DNS 服务器失去域名的 IP 地址
  • 向这个 IP 的机器发送 HTTP 申请
  • 服务器收到、解决并返回 HTTP 申请
  • 浏览器失去返回内容

例如在浏览器输出 https://juejin.im/timeline,而后通过 DNS 解析,juejin.im 对应的 IP 是36.248.217.149(不同工夫、地点对应的 IP 可能会不同)。而后浏览器向该 IP 发送 HTTP 申请。

服务端接管到 HTTP 申请,而后通过计算(向不同的用户推送不同的内容),返回 HTTP 申请,返回的内容如下:

其实就是一堆 HMTL 格局的字符串,因为只有 HTML 格局浏览器能力正确解析,这是 W3C 规范的要求。接下来就是浏览器的渲染过程。

浏览器渲染过程

浏览器渲染过程大体分为如下三局部:

1)浏览器会解析三个货色:

  • 一是 HTML/SVG/XHTML,HTML 字符串形容了一个页面的构造,浏览器会把 HTML 构造字符串解析转换 DOM 树形构造。

  • 二是 CSS,解析 CSS 会产生 CSS 规定树,它和 DOM 构造比拟像。

  • 三是 Javascript 脚本,等到 Javascript 脚本文件加载后,通过 DOM API 和 CSSOM API 来操作 DOM Tree 和 CSS Rule Tree。

2)解析实现后,浏览器引擎会通过 DOM Tree 和 CSS Rule Tree 来结构 Rendering Tree。

  • Rendering Tree 渲染树并不等同于 DOM 树,渲染树只会包含须要显示的节点和这些节点的款式信息。
  • CSS 的 Rule Tree 次要是为了实现匹配并把 CSS Rule 附加上 Rendering Tree 上的每个 Element(也就是每个 Frame)。
  • 而后,计算每个 Frame 的地位,这又叫 layout 和 reflow 过程。

3)最初通过调用操作系统 Native GUI 的 API 绘制。

接下来咱们针对这其中所经验的重要步骤具体论述

构建 DOM

浏览器会恪守一套步骤将 HTML 文件转换为 DOM 树。宏观上,能够分为几个步骤:

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

在网络中传输的内容其实都是 0 和 1 这些字节数据。当浏览器接管到这些字节数据当前,它会将这些字节数据转换为字符串,也就是咱们写的代码。

  • 将字符串转换成 Token,例如:<html><body>等。Token 中会标识出以后 Token 是“开始标签”或是“完结标签”亦或是“文本”等信息

这时候你肯定会有疑难,节点与节点之间的关系如何保护?

事实上,这就是 Token 要标识“起始标签”和“完结标签”等标识的作用。例如“title”Token 的起始标签和完结标签之间的节点必定是属于“head”的子节点。

上图给出了节点之间的关系,例如:“Hello”Token 位于“title”开始标签与“title”完结标签之间,表明“Hello”Token 是“title”Token 的子节点。同理“title”Token 是“head”Token 的子节点。

  • 生成节点对象并构建 DOM

事实上,构建 DOM 的过程中,不是等所有 Token 都转换实现后再去生成节点对象,而是一边生成 Token 一边耗费 Token 来生成节点对象。换句话说,每个 Token 被生成后,会立即耗费这个 Token 创立出节点对象。留神:带有完结标签标识的 Token 不会创立节点对象。

接下来咱们举个例子,假如有段 HTML 文本:

<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 会解析成这样:

构建 CSSOM

DOM 会捕捉页面的内容,但浏览器还须要晓得页面如何展现,所以须要构建 CSSOM。

构建 CSSOM 的过程与构建 DOM 的过程十分类似,当浏览器接管到一段 CSS,浏览器首先要做的是辨认出 Token,而后构建节点并生成 CSSOM。

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

留神:CSS 匹配 HTML 元素是一个相当简单和有性能问题的事件。所以,DOM 树要小,CSS 尽量用 id 和 class,千万不要过渡层叠上来

构建渲染树

当咱们生成 DOM 树和 CSSOM 树当前,就须要将这两棵树组合为渲染树。

在这一过程中,不是简略的将两者合并就行了。渲染树只会包含须要显示的节点和这些节点的款式信息,如果某个节点是 display: none 的,那么就不会在渲染树中显示。

咱们或者有个纳闷:浏览器如果渲染过程中遇到 JS 文件怎么解决

渲染过程中,如果遇到 <script> 就进行渲染,执行 JS 代码。因为浏览器有 GUI 渲染线程与 JS 引擎线程,为了避免渲染呈现不可预期的后果,这两个线程是互斥的关系。JavaScript 的加载、解析与执行会阻塞 DOM 的构建,也就是说,在构建 DOM 时,HTML 解析器若遇到了 JavaScript,那么它会暂停构建 DOM,将控制权移交给 JavaScript 引擎,等 JavaScript 引擎运行结束,浏览器再从中断的中央复原 DOM 构建。

也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件,这也是都倡议将 script 标签放在 body 标签底部的起因。当然在当下,并不是说 script 标签必须放在底部,因为你能够给 script 标签增加 defer 或者 async 属性(下文会介绍这两者的区别)。

JS 文件不只是阻塞 DOM 的构建,它会导致 CSSOM 也阻塞 DOM 的构建

本来 DOM 和 CSSOM 的构建是互不影响,井水不犯河水,然而一旦引入了 JavaScript,CSSOM 也开始阻塞 DOM 的构建,只有 CSSOM 构建结束后,DOM 再复原 DOM 构建。

这是什么状况?

这是因为 JavaScript 不只是能够改 DOM,它还能够更改款式,也就是它能够更改 CSSOM。因为不残缺的 CSSOM 是无奈应用的,如果 JavaScript 想拜访 CSSOM 并更改它,那么在执行 JavaScript 时,必须要能拿到残缺的 CSSOM。所以就导致了一个景象,如果浏览器尚未实现 CSSOM 的下载和构建,而咱们却想在此时运行脚本,那么浏览器将提早脚本执行和 DOM 构建,直至其实现 CSSOM 的下载和构建。也就是说,在这种状况下,浏览器会先下载和构建 CSSOM,而后再执行 JavaScript,最初在持续构建 DOM

布局与绘制

当浏览器生成渲染树当前,就会依据渲染树来进行布局(也能够叫做回流)。这一阶段浏览器要做的事件是要弄清楚各个节点在页面中的确切地位和大小。通常这一行为也被称为“主动重排”。

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

布局实现后,浏览器会立刻收回“Paint Setup”和“Paint”事件,将渲染树转换成屏幕上的像素。

以上咱们具体介绍了浏览器工作流程中的重要步骤,接下来咱们探讨几个相干的问题:

几点补充阐明

1.async 和 defer 的作用是什么?有什么区别?

接下来咱们比照下 defer 和 async 属性的区别:

其中蓝色线代表 JavaScript 加载;红色线代表 JavaScript 执行;绿色线代表 HTML 解析。

1)状况 1 <script src="script.js"></script>

没有 defer 或 async,浏览器会立刻加载并执行指定的脚本,也就是说不期待后续载入的文档元素,读到就加载并执行。

2)状况 2 <script async src="script.js"></script> (异步下载)

async 属性示意异步执行引入的 JavaScript,与 defer 的区别在于,如果曾经加载好,就会开始执行——无论此刻是 HTML 解析阶段还是 DOMContentLoaded 触发之后。须要留神的是,这种形式加载的 JavaScript 仍然会阻塞 load 事件。换句话说,async-script 可能在 DOMContentLoaded 触发之前或之后执行,但肯定在 load 触发之前执行。

3)状况 3 <script defer src="script.js"></script>(提早执行)

defer 属性示意提早执行引入的 JavaScript,即这段 JavaScript 加载时 HTML 并未进行解析,这两个过程是并行的。整个 document 解析结束且 defer-script 也加载实现之后(这两件事件的程序无关),会执行所有由 defer-script 加载的 JavaScript 代码,而后触发 DOMContentLoaded 事件。

defer 与相比一般 script,有两点区别:** 载入 JavaScript 文件时不阻塞 HTML 的解析,执行阶段被放到 HTML 标签解析实现之后。
在加载多个 JS 脚本的时候,async 是无程序的加载,而 defer 是有程序的加载。**

2. 为什么操作 DOM 慢

把 DOM 和 JavaScript 各自设想成一个岛屿,它们之间用免费桥梁连贯。——《高性能 JavaScript》

JS 是很快的,在 JS 中批改 DOM 对象也是很快的。在 JS 的世界里,所有是简略的、迅速的。但 DOM 操作并非 JS 一个人的独舞,而是两个模块之间的合作。

因为 DOM 是属于渲染引擎中的货色,而 JS 又是 JS 引擎中的货色。当咱们用 JS 去操作 DOM 时,实质上是 JS 引擎和渲染引擎之间进行了“跨界交换”。这个“跨界交换”的实现并不简略,它依赖了桥接接口作为“桥梁”(如下图)。

过“桥”要免费——这个开销自身就是不可疏忽的。咱们每操作一次 DOM(不论是为了批改还是仅仅为了拜访其值),都要过一次“桥”。过“桥”的次数一多,就会产生比拟显著的性能问题。因而“缩小 DOM 操作”的倡议,并非空穴来风。

3. 你真的理解回流和重绘吗

渲染的流程基本上是这样(如下图黄色的四个步骤):1. 计算 CSS 款式 2. 构建 Render Tree 3.Layout – 定位坐标和大小 4. 正式开画

留神:上图流程中有很多连接线,这示意了 Javascript 动静批改了 DOM 属性或是 CSS 属性会导致从新 Layout,但有些扭转不会从新 Layout,就是上图中那些指到天上的箭头,比方批改后的 CSS rule 没有被匹配到元素。

这里重要要说两个概念,一个是 Reflow,另一个是 Repaint

  • 重绘:当咱们对 DOM 的批改导致了款式的变动、却并未影响其几何属性(比方批改了色彩或背景色)时,浏览器不需从新计算元素的几何属性、间接为该元素绘制新的款式(跳过了上图所示的回流环节)。
  • 回流:当咱们对 DOM 的批改引发了 DOM 几何尺寸的变动(比方批改元素的宽、高或暗藏元素等)时,浏览器须要从新计算元素的几何属性(其余元素的几何属性和地位也会因而受到影响),而后再将计算的后果绘制进去。这个过程就是回流(也叫重排)

咱们晓得,当网页生成的时候,至多会渲染一次。在用户拜访的过程中,还会一直从新渲染。从新渲染会反复回流 + 重绘或者只有重绘。
回流必定会产生重绘,重绘不肯定会引发回流。重绘和回流会在咱们设置节点款式时频繁呈现,同时也会很大水平上影响性能。回流所需的老本比重绘高的多,扭转父节点里的子节点很可能会导致父节点的一系列回流。

1)常见引起回流属性和办法

任何会扭转元素几何信息 (元素的地位和尺寸大小) 的操作,都会触发回流,

  • 增加或者删除可见的 DOM 元素;
  • 元素尺寸扭转——边距、填充、边框、宽度和高度
  • 内容变动,比方用户在 input 框中输出文字
  • 浏览器窗口尺寸扭转——resize 事件产生时
  • 计算 offsetWidth 和 offsetHeight 属性
  • 设置 style 属性的值

2)常见引起重绘属性和办法

3)如何缩小回流、重绘

  • 应用 transform 代替 top
  • 应用 visibility 替换 display: none,因为前者只会引起重绘,后者会引发回流(扭转了布局)
  • 不要把节点的属性值放在一个循环里当成循环里的变量。
for(let i = 0; i < 1000; i++) {
    // 获取 offsetTop 会导致回流,因为须要去获取正确的值
    console.log(document.querySelector('.test').style.offsetTop)
}
  • 不要应用 table 布局,可能很小的一个小改变会造成整个 table 的从新布局
  • 动画实现的速度的抉择,动画速度越快,回流次数越多,也能够抉择应用 requestAnimationFrame
  • CSS 选择符从右往左匹配查找,防止节点层级过多
  • 将频繁重绘或者回流的节点设置为图层,图层可能阻止该节点的渲染行为影响别的节点。比方对于 video 标签来说,浏览器会主动将该节点变为图层。

性能优化策略

基于下面介绍的浏览器渲染原理,DOM 和 CSSOM 构造构建程序,初始化能够对页面渲染做些优化,晋升页面性能。

  • JS 优化:<script> 标签加上 defer 属性 和 async 属性 用于在不阻塞页面文档解析的前提下,管制脚本的下载和执行。

    • defer 属性:用于开启新的线程下载脚本文件,并使脚本在文档解析实现后执行。
    • async 属性:HTML5 新增属性,用于异步下载脚本文件,下载结束立刻解释执行代码。
  • CSS 优化:<link> 标签的 rel 属性 中的属性值设置为 preload 可能让你在你的 HTML 页面中能够指明哪些资源是在页面加载实现后即刻须要的, 最优的配置加载程序,进步渲染性能

总结

综上所述,咱们得出这样的论断:

  • 浏览器工作流程:构建 DOM -> 构建 CSSOM -> 构建渲染树 -> 布局 -> 绘制。
  • CSSOM 会阻塞渲染,只有当 CSSOM 构建结束后才会进入下一个阶段构建渲染树。
  • 通常状况下 DOM 和 CSSOM 是并行构建的,然而当浏览器遇到一个不带 defer 或 async 属性的 script 标签时,DOM 构建将暂停,如果此时又凑巧浏览器尚未实现 CSSOM 的下载和构建,因为 JavaScript 能够批改 CSSOM,所以须要等 CSSOM 构建结束后再执行 JS,最初才从新 DOM 构建。

正文完
 0