一、渲染流程
bash
下面我们将详细介绍浏览器的渲染流程。通常来说,浏览器渲染流程包括以下几个步骤:
1. 解析 HTML
当浏览器向服务器获取 HTML 文档的请求时,服务器返回二进制流格式的 HTML 页面,响应头 Content-Type 值为 text/html,这里 text/html 是 MIME 类型,它告诉浏览器它是一个 HTML 文档。
当浏览器收到 HTML 数据时,主线程开始解析文本字符串(HTML)并将其转换为文档对象模型(DOM),在浏览器开发者工具 console 中 输入 document 即可看到一个完整的 DOM 树结构。
- 当 HTML 解析器发现
<script>
标签时,会暂停 HTML 文档的解析,并必须加载、解析和执行JavaScript代码,即<script>
标签会阻塞 DOM 的解析和渲染。
<link>
标签不会阻塞 DOM 的解析,但是会阻塞 DOM 的渲染,同时还会阻塞之后的<script>
标签的执行。<link rel="preload">
preload 属性 会预加载资源。<link rel="dns-prefetch">
dns-prefetch 属性 (DNS 预获取) 是尝试在请求资源之前解析域名,仅对跨域域上的 DNS 查找有效。
2. 样式计算(Style)
主线程解析 CSS 并确定每个 DOM 节点的计算样式,目的是为了计算出 DOM 节点中每个元素的具体样式。经过计算,每个元素节点都会有一个最终样式,在浏览器开发者工具 Elements -> Computed 即可查看。
样式计算主要有以下三个步骤:
2.1 把 CSS 转换为 styleSheets
CSS 样式主要来源:
<link>
标签<style>
标签- 元素的Style属性
浏览器无法直接理解纯文本的CSS样式,当渲染引擎接收到 CSS 文本时,会将 CSS 文本转换为浏览器可以理解的 styleSheets 结构。<br />在浏览器开发者工具 console 中 输入 document.styleSheets 即可看到所有解析后 CSS 样式。<br />如果不提供任何 CSS,也会有默认样式。 如 <h1> 标签显示大于 <h2>
标签,这是因为浏览器具有默认样式表。如果你想知道 Chrome 的默认 CSS 是什么样的,你可以在这里看到源代码。
2.2 标准化样式表中的属性值
css
可以看到上面的 CSS 文本中有很多属性值,如 2em、blue、bold,这些属性值不被渲染引擎理解,需要将所有值转换为渲染引擎容易理解的、标准化的计算值,这个过程就是属性值标准化。如 2em 被解析成 32px,red 被解析成了 rgb(255, 0, 0),bold 被解析为 700。
2.3 计算出每个 DOM 节点的具体样式
每个 DOM 节点的样式属性,是 CSS 继承规则和层叠规则 进行叠加计算的结果。层叠是 CSS 的一个基本特征,继承指的是某些样式属性会从父元素传递给子元素,而层叠则是指当多个样式规则应用到同一个元素时,浏览器根据一定的规则确定最终的样式值。
3. 布局(Layout)
现在,我们知道文档的结构和每个节点的样式,但这还不足以显示页面,因为我们还不知道 DOM 元素的几何位置信息。 布局阶段是浏览器确定每个元素在页面中的位置和大小的过程。在布局过程中,浏览器会根据元素的盒模型、样式属性等计算出每个元素的位置和尺寸。
Chrome 在布局阶段需要完成两个任务:创建布局树和布局计算。
3.1 创建布局树
遍历 DOM 树中的每一个节点,过滤不可见的元素(比如 head 标签,比如具有 display:none 样式的元素),将所有的可见元素构建一棵只包含可见元素布局树(具有 visibility: hidden 的元素会在布局树中)。
3.2 布局计算
有了布局树之后,就需要对布局树中各节点的几何坐标位置进行计算,其中包含 x,y 坐标和边界框大小等信息。
4. 分层(Layer)
主线程遍历布局树,根据策略对布局树进行分层,并生成一棵对应的图层树。Chrome 引入了分层和合成的机制就是为了提升每帧的渲染效率。
在浏览器开发者工具 (Layers) 可以查看可视化页面的分层情况,如下图所示:
什么样的节点会创建独立的图层?
- position 属性值为 fixed 或者 sticky
- opacity 属性值小于 1
- z-index 属性值不为 auto
- transform、filter、perspective、clip-path 属性值不为 none
- 在 will-change 中指定了任意CSS属性
5. 绘制(Paint)
有了图层树后,主线程会为每个图层单独绘制指令集,用于描述这一层的内容该如何画出来。如把画笔移到某个位置,先画什么再画什么,把一个图层的绘制拆分成很多小的 绘制指令 ,然后再把这些指令按照顺序组成一个 待绘制列表。<br />主线程将每个图层的绘制信息提交给合成线程(compositor thread) ,剩余工作将由合成线程完成。
6. 分块(Tiling)
浏览器渲染分块用于将页面分割为多个独立的块,然后分别渲染这些块。每个块都是独立的渲染区域,并且可以单独更新和绘制,以提高渲染性能和响应速度。
- 页面分割: 浏览器将页面划分为多个独立的块。通常,这些块的大小是固定的,例如 256x256 像素。每个块都有自己的位置和尺寸。
- 多线程处理: 浏览器可以使用多个线程来并行处理分块。这使得浏览器能够更高效地利用硬件资源。
7. 光栅化(Raster)
光栅化是将页面上的图形、文本和其他可见元素转换为像素的过程。在浏览器中,页面的可视内容通常以矢量形式表示,但在显示器上呈现时需要将其转换为光栅图像(由像素组成的位图)。 合成线程将分块信息交给 GPU 进程,GPU 进程会开启多个线程来完成光栅化,并且优先处理靠近视口区域的块,将块生成位图。
8. 画(Draw)
合成线程拿到每个图层、块的位图后,生产一个个 指引(quad) 信息,指引会标识出每个位图应该画在屏幕的哪个位置,以及会考虑旋转,缩放等变形。 transform 发生在合成线程,与渲染主线程无关,所以这就是 transform 效率高的本质原因。
二、优化
1. 重排(reflow)
reflow 的本质是重新计算 layout 树,以下的操作都会导致页面 reflow :
- 页面首次渲染。
- 浏览器窗口大小发生变化。
- 元素的内容发生变化。
- 元素的尺寸或者位置发生变化。
- 元素的字体大小发生变化。
- 激活 CSS 伪类。
- 查询某些属性或者调用某些方法。
- 添加或者删除可见的 DOM 元素。
为了避免连续的多次操作导致布局树反复计算,浏览器会合并这些操作,当 JS 代码全部完成后再进行计算,所以改动属性造成的 reflow 是异常完成,也因此 当 JS 获取布局属性时,可能无法获取到最新的布局信息。 浏览器在反复权衡下,最终决定获取属性立即 reflow。 如调用 dom.clientWidth。
2. 重绘(repaint)
repaint 的本质就是重新根据分层信息计算绘制指令,以下的操作会导致 repaint:
- 改变 color、background 相关属性:background-color、background-image。
- 改变 outline 相关属性:outline-color、outline-width 、text-decoration。
- 改变 border-radius、visibility、box-shadow。
当改动了可见样式后,就需要重新计算,会引发 repaint。<br />元素的布局信息也属于可见样式,所以 reflow 一定会引起 repaint。
3. 为什么 transform 效率高
因为 transform 既不会影响布局也不会影响绘制指令,它只会影响渲染流程的最后一步 draw 阶段,由于 draw 在合成线程中,所以 transform 的变化几乎不会影响到渲染主线程。但是必须结合图层做,will-change: transform
4. 其他优化的点
1、采用 opacity 代替 visibility,其机制和 transform 一致 2、将多次改变样式的操作合并成一次 3、利用文档碎片(documentFragment) 4、不要把获取某些 DOM 节点的属性值放在一个循环里面当成循环的变量
当获取文档的 width.height 的时候会触发重绘和重排
5、为动画元素新建图层