这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战
前几篇讲了关于url和请求的一些过程,今天来学习以下浏览器在渲染页面时的过程。
一、概括
浏览器的渲染流程可以概括成下列步骤:
- 读取HTML文件构建 DOM树(DOM tree) 和 CSSDOM树(CSSDOM tree)。
- 遇到 JS文件则阻塞 DOM树 和 CSSDOM树 的构建,优先加载 JS文件。
- 构建 渲染树(render tree),并渲染到页面上。
- 渲染树节点发生改变时,进行页面的 重绘(repaint) 或 回流
二、详解
1. 构建 DOM树 和 CSSDOM树
HTML文档中有着许许多多的节点,如:文档节点、元素节点、文本节点、属性节点、注释节点等。HTML文档是自上而下解析的。 浏览器读取HTML文件后,会经过以下流程:
读取文档(字节) -> 转为字符 -> 变成标签 -> 转为节点 -> 构建 DOM树
<html>
<head>
<meta charset="utf-8" />
<link href="./asdf.js" />
<script src="https://afsadfljaslj.js"></script>
</head>
<body>
<div>
<h1>Hello World</h1>
</div>
</body>
</html>
复制代码
CSSDOM树 的构建与 DOM树的构建类似,因为CSS文档中,所有元素也都是节点,且与HTML文档中的标签节点一一对应,各个节点中也有着层级关系,如兄弟,父子,子孙等。构成 CSSDOM树。流程为:
读取文档(字节) -> 转为字符 -> 变成标签 -> 转为节点 -> 构建 CSSDOM树
2. JS加载和阻塞
当遇到JS文件时,会阻塞 DOM树 和 CSSDOM树 的构建,优先加载 JS文件,是因为浏览器认为如果同时进行对 DOM树 的构建的话,若 JS 中存在对 DOM进行操作的代码的话,则浪费了资源。如:
<html>
<head>
<meta charset="utf-8" />
<link href="./asdf.js" />
<script src="./script.js"></script>
</head>
<body>
<div>
<h1>Hello World</h1>
</div>
</body>
</html>
复制代码
console.log(123);
document.querySelect("h1").innerText = "你好";
复制代码
如果加载一个体量过大的 JS文件,可能需要用到 1秒、2秒,甚至5秒的可能,那么对 DOM树 和 CSSDOM树 的构建则就耽误了,页面没能及时渲染到浏览器上,用户体验感极差。
对此有两种解决方法:
- 因为 HTML文档 是自上而下解析的,所以将
<script>
标签放在文档最底下(适用于内部文件) - 添加
async
和defer
属性进行异步加载 JS文件(适用于外部文件)
关于 asnyc
和 defer
:
async
:(适用于 JS文件 不关心页面 DOM元素 时)- 脚本会在加载完毕后执行。
async
脚本的加载不计入DOMContentLoaded
事件统计。
- 脚本会在加载完毕后执行。
defer
:(适用于 JS文件 较关心页面 DOM元素 时)- 文档解析时,遇到设置了
defer
的脚本,就会在后台进行下载,但是并不会阻止文档的渲染,当页面解析&渲染完毕后。会等到所有的defer
脚本加载完毕并按照顺序执行,执行完毕后会触发DOMContentLoaded
事件。
- 文档解析时,遇到设置了
- 不知道的情况下,用
defer
总会安全点。
3. 构建渲染树
渲染树 有 DOM树 和 CSSDOM树 合并而成,三者的构建顺序并无先后,而是并行构建的,形成一边解析一边构建一边渲染的情况,所以有时我们会看到页面渲染出来了一半,另一半还是空白的情况。
4. 重绘和回流
当渲染树中的某个节点的可视化属性(看得见的改变)发生了改变,则会引发页面的重绘和回流,两者的区别是:
- 重绘(repaint):
- 渲染树节点发生变化,但并不影响该节点在页面种的空间位置以及大小,如:背景颜色、字体颜色等,但是该节点的宽、高、内外边距并没有改变,此时触发浏览器的 重绘。
- 回流(reflow):
- 也称为重排,当渲染树节点发生变化,且影响到了该节点的几何属性(如宽、高、内边距、外边距、或是float、position、display:none;等等),或导致节点发生了位置变化(如
left
、top
、bottom
、right
等),此时出发浏览器的 回流。需要重新构建渲染树,且该节点之后的所有节点都会发生改变,重新布局。
- 也称为重排,当渲染树节点发生变化,且影响到了该节点的几何属性(如宽、高、内边距、外边距、或是float、position、display:none;等等),或导致节点发生了位置变化(如
注意:回流必然引起重绘,而重绘比一定引起回流。
一些以前回流的情况:
- 添加或者删除可见的DOM元素;
- 元素位置改变——display、float、position、overflow等等;
- 元素尺寸改变——边距、填充、边框、宽度和高度
- 内容改变——比如文本改变或者图片大小改变而引起的计算值宽度和高度改变;
- 页面渲染初始化;
- 浏览器窗口尺寸改变——resize事件发生时;
可见,回流 消耗的资源比 重绘 要大得多。
三、其他
1. 不同内核的浏览器的渲染流程
不同浏览器的渲染流程在细节上是不同的,如:
这是 webkit 的主要流程:
这是 Geoko 的主要流程:
虽然在一些地方有所不同,但渲染主流程还是一样的。
2. 如何减少和避免回流
- CSS:
- 避免设置多层的内联样式
- 尽可能在 DOM树最末端 改变 class
- 若要设置动画效果,最好脱离正常文档流
- 避免使用 CSS表达式(如:calc())
- 用
visibility: hidden
(渲染树上有保留) 代替display: none
(渲染树上会删除掉)
- JS
- 避免频繁操作 style,最好用一个class一次性更改样式
- 避免频繁操作 DOM,必要的话使用
documentFragment
- 可以使用“DOM离线操作”(将该元素设为 display:none,后操作结束再显示出来,这样只进行两次回流),但建议还是用第一个方法。
有什么问题希望大家可以在评论区指出,我及时纠正。
新人上路,还请多多包含。
我是MoonLight,一个初出茅庐的小前端。