聊聊浏览器页面渲染机制

前沿

浏览器内核是浏览器运行的最核心部分,一般分为两个部分,一个是渲染引擎,另一个是JavaScript引擎。
目前市面上常见的浏览器内核可以分为这四种:Trident(IE)、Gecko(火狐)、Blink(Chrome、Opera)、Webkit(Safari)。
本文我们就以 Webkit 为例,对现代浏览器的渲染过程进行一个深度的剖析。

页面加载过程

我们首先简单介绍一下页面的加载过程
大致如下:

浏览器根据 DNS 服务器得到域名的 IP 地址
向这个 IP 的机器发送 HTTP 请求
服务器收到处理并返回 HTTP 请求
浏览器得到返回内容

如果我们请求的是一个页面,返回的就是一堆HTML格式的字符串。
http请求结果

浏览器渲染过程

浏览器渲染大致分为三个过程

(1)浏览器解析三种文件
  • 首先是HTML/SVG/XHTML,HTML字符串描述了一个页面的结构,浏览器会把HTML结构字符串解析转换DOM树形结构。
HTML
HTML Parser
DOM 结构树
  • 二是CSS,解析CSS会产生CSS规则树,它和DOM结构比较像。
CSS
CSS Parser
CSSOM 规则树
  • 三是Javascript脚本,等到Javascript 脚本文件加载后, 通过 DOM API 和 CSSOM API 来操作 DOM Tree 和 CSS Rule Tree。
document.getElementById
HTML
HTML Parser
DOM 结构树
JS
JS Engine
(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

**DOM (Document Object Model): ** 文档对象模型。基于 DOM 表示的文档被描述成一个树形结构,DOM 提供了接口让 JavaScript 修改 HTML 文档。

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

字节数据
字符串
Token
Node
DOM
  • 转换(Conversion): 浏览器从网络读取HTML的原始字节,并根据文件的指定编码(例如 UTF-8)将它们转换成字符串。
    在网络中传输的内容其实都是 0 和 1 这些二进制字节数据。当浏览器接收到这些字节数据以后,它会将这些字节数据转换为字符串,也就是我们写的代码。
  • 令牌化(Tokenizing): 将字符串转换成Token,例如: <html>、 <body>等。Token中会标识出当前Token是“开始标签”或是“结束标签”亦或是“文本”等信息。
  • 词法分析(Lexing): 将 token 转换成定义其属性和规则的“对象”。其实就是生成节点对象
  • 构建DOM tree
    DOM tree中定义元素的父——子关系:HTML 是 body 的父对象,body 是段落的父对象。
    事实上,构建DOM的过程中,不是等所有Token都转换完成后再去生成节点对象,而是一边生成Token一边消耗Token来生成节点对象。换句话说,每个Token被生成后,会立刻消耗这个Token创建出节点对象。注意:带有结束标签标识的Token不会创建节点对象。
<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>

浏览器渲染过程

构建CSSOM

CSSOM(CSS Object Model): CSS 对象模型。CSSOM 定义了 JavaScript 访问样式的能力和方式。CSSOM 提供了接口让 JavaScript 获得和修改 CSS 代码设置的样式信息。

DOM会捕获页面的内容,但浏览器还需要知道页面如何展示,所以需要构建CSSOM。

与处理 HTML 时类似,需要将收到的 CSS 规则转换成某种浏览器能够理解和处理的内部表示。因此会重复构建 HTML 过程,不过是对 CSS文件 。

字节数据
字符串
Token
Node
CSSOM

在这一过程中,浏览器会确定下每一个节点的样式到底是什么,并且这一过程其实是很消耗资源的。因为样式你可以自行设置给某个节点,也可以通过继承获得。在这一过程中,浏览器得递归 CSSOM 树,然后确定具体的元素到底是什么样式。
注意:CSS匹配HTML元素是一个相当复杂和有性能问题的事情。所以,DOM树要小,CSS尽量用id和class,千万不要过渡层叠下去。

构建渲染树(RenderObject tree)

当我们生成 DOM 树和 CSSOM 树以后,就需要将这两棵树组合为渲染树。
渲染树

渲染树(RenderObject tree)是基于 DOM tree 建立起来的一棵新树

渲染树只会包括需要显示的节点这些节点的样式信息,如果某个节点是 display:none 的,那么就不会在渲染树中显示。


浏览器如果渲染过程中遇到JS文件怎么处理???

渲染过程中,如果遇到 <script>就停止渲染,执行 JS 代码。因为浏览器渲染和 JS 执行共用一个线程,而且这里必须是单线程操作,多线程会产生渲染 DOM 冲突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。

DOM构建


布局、绘图和渲染层级树

布局

当浏览器创建 渲染树(RenderObject tree) 对象之后,每个对象并不知道自己在设备视口内的位置、大小等信息。浏览器根据盒模型(Box-model)来计算它们的位置、大小等信息的过程称为布局计算(重排)。

浏览器会从 渲染树(RenderObject tree) 的根节点开始进行遍历。

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

渲染层级树(RenderLayer tree)

浏览器渲染引擎并不是直接使用 渲染树(RenderObject tree) 进行绘制,为了方便处理 Positioning(定位),Clipping(裁剪),Overflow-scroll(页內滚动),CSS Transform/Opacity/Animation/Filter,Mask or Reflection,Z-indexing(Z 排序)等,浏览器需要会为一些特定的 RenderObject 生成对应的 RenderLayer,并生成一棵对应的 RenderLayer tree。

那需要满足什么条件,渲染引擎才为 RenderObject 建立对应的 RenderLayer?

  • 显式指定 CSS position 属性的 RenderObject 节点;
  • 有透明度的 RenderObject 节点;
  • 有 overflow,alpha 和 reflection 的样式 RenderObject 节点;
  • 有 filter 样式的 RenderObject 节点;
  • 使用 Canvas 2D 和 3D(WebGL)技术的 RenderObject 节点;
  • video 元素对应的 RenderObject 节点;
    。。。。。。

每个 RenderLayer 对象可以想象成图像中一个图层,各个图层叠加构成了一个图像。

浏览器会遍历 RenderLayer tree,再遍历从属这个 RenderLayer 的 RenderObject,RenderObject 对象存储有绘制信息,并进行绘制。

RenderLayer 和 RenderObject 共同决定了最终呈现的网页内容,RenderLayer tree 决定了网页的绘制的层次顺序,而从属于 RenderLayer 的 RenderObject 决定了该 RenderLayer 的内容。

RenderLayer

绘图(Paint)

在完成构建 RenderLayer tree 之后,浏览器会使用图形库将其构建的渲染模型绘制出来,该过程分为两个阶段:

  • 绘制: 将从属每个 RenderLayer 图层上的 RenderObject 绘制在其 RenderLayer 上。即绘制(Paint)或者光栅化(Rasterization),将一些绘图指令转换成真正的像素颜色值。
    • 软件绘图:CPU 来完成绘图操作
    • 硬件加速绘图:GPU 来完成绘图操作
  • 合成(compositing): 将各个 RenderLayer 图层合并成到一个位图(Bitmap)中。同时还可能包括位移(Translation),缩放(Scale),旋转(Rotation),Alpha 合成等操作。

渲染方式(Rendering)

  1. 软件渲染方式
  2. 硬件加速渲染的合成化渲染方式
  3. 软件绘图的合成化渲染方式

补充说明

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

1)情况1 <scriptsrc=“script.js”></script>

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

2)情况12 <script async src=“script.js”> (异步下载)

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

3)情况3 <scriptdefersrc=“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.绘图(Pairt); 5.合成(composite)

Content
Compute style
Construct frames
Layout
Pairt
Composite

了解两个概念,一个是Reflow,另一个是Repaint。

  • 重绘:当我们对 DOM 的修改导致了样式的变化、却并未影响其几何属性(比如修改了颜色或背景色)时,浏览器不需重新计算元素的几何属性、直接为该元素绘制新的样式。
  • 回流:当我们对 DOM 的修改引发了 DOM 几何尺寸的变化(比如修改元素的宽、高或隐藏元素等)时,浏览器需要重新计算元素的几何属性(其他元素的几何属性和位置也会因此受到影响),然后再将计算的结果绘制出来。这个过程就是回流(也叫重排)

网页加载后,绘制新的每一帧,一般都需要经过计算布局(layout)、绘图(paint)、合成(composite)三阶段。

而在这三个阶段中,layout 和 paint 比较耗时间,而合成需要的时间相对较少一些。

回流(reLayout)

如果修改 DOM 元素的 layout 样式(如 width, heihgt 等),浏览器会计算页面需要 回流 的元素,然后触发一个 reLayout。被 reLayout的元素,接着会执行绘制,最后进行渲染合并生成页面。

JavaScript
Style
Layout
Pairt
Composite

重绘(rePaint)

如果修改 DOM 元素的 paint 样式(如 color, background 等),浏览器会跳过布局,直接执行绘制,再进行合成。

JavaScript
Style
Layout
Pairt
Composite

合成(composite)

如果修改 DOM 元素的 composite 样式(如 transform, opacity 等)。浏览器会跳过布局和绘制,直接执行合成。该过程是开销最小的,也是优化着手点。

JavaScript
Style
Layout
Pairt
Composite

重绘和回流会在我们设置节点样式时频繁出现,同时也会很大程度上影响性能。回流所需的成本比重绘高的多,改变父节点里的子节点很可能会导致父节点的一系列回流。

如果想知道修改任何指定 CSS 样式会触发 layout、paint、composite 中的哪一个,请查看CSS 触发器

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

任何会改变元素几何信息(元素的位置和尺寸大小)的操作,都会触发回流:

  • 添加或者删除可见的DOM元素;
  • 元素尺寸改变——边距、填充、边框、宽度和高度;
  • 内容变化,比如用户在input框中输入文字;
  • 浏览器窗口尺寸改变——resize事件发生时;
  • 计算 offsetWidth 和 offsetHeight 属性;
2) 如何减少回流、重绘
  • 使用合成属性样式(opcity、tansform)来完成 tansition 或 animation。
  • 使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局)
  • 不要把节点的属性值放在一个循环里当成循环里的变量。
for(var i = 0; i < 100; i++){
//offsetTop会导致回流,因为要去获取值。
	console.log(document.querySelector('.name').style.offsetTop);
}
  • 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局。
  • 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame
  • CSS 选择符从右往左匹配查找,避免节点层级过多

参考文章

前端工匠
singsong

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值