浏览器如何渲染一个 html 文件?
浏览器如何渲染一个 html 文件
14KB 规则,具体的看 MDN
的解释:https://developer.mozilla.org/zh-CN/docs/Web/Performance/How_browsers_work#tcp_慢启动_14kb_规则大概就是 TCP 在响应浏览器的请求时,第一个数据包大概为 14KB 然后下一个为 28KB, 接着为 56KB,
后续的包是前一个包大小的两倍,直到达到预定的阈值,或者遇到阻塞。 可以这么理解一下,服务器端并不知道我们的宽带是多少,所以使用这样的方式来进行「试探」。
所以就可以针对这一点进行优化,比如:在首个 14KB 中包含尽可能多的渲染页面所需的内容。
参考:
疑问:
在构建 DOM 树和 CSSOM 树章节中缺少使用 @import 引入 css 的分析;之前看到的所有文章都说 @import 会等到页面完全载入后才开始加载;我不太明白这个意思;但是我使用这个例子, link 与 @import 依然是并行加载的,而且发起请求的时间差不多是同时:
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.2.3/css/bootstrap-utilities.css" type="text/css"
rel="stylesheet">
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.2.3/css/bootstrap.css" type="text/css"
rel="stylesheet">
<style>
@import url("https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.2.3/css/bootstrap-grid.css");
@import url("https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.2.3/css/bootstrap-reboot.css");
</style>
<body>
<div>div</div>*10000
</body>
构建 DOM 树
注意:以下将使用到 DOMContentLoaded 触发这个事件时,可以大概认为 DOM 树已经构建完成;
首先浏览器接受服务器返回到字节数据,然后通过转码将这些数据转换为字符串;为什么是一部分呢?因为这个过程是渐进的,如果 html
文件很大,只传输了一部分,那么就会从这一部分开始,而不是等到所有的 html 文件传输完成才开始解析。
然后会根据字符串生成对应的 tokens;tokens 的形式其实就是「开始标签」和「结束标签」,比如
<html>
<body></body>
</html>
这样的 html 结构,生成的 tokens 大致的形式为,也就标识了节点的层级结构
[startTag html]
[startTag body]
[endTag body]
[endTag html]
tokens 生成后就会直接开始创建 html 元素对象,然后构建 DOM 树;
但是在这里我有一个疑问,是 tokens 生成后就立马开始解析创建元素对象,还是 endTag 也生成后才开始解析的?
进行了一个测试,有一个很大的 html 文件例如:
<html>
<body>
<div>top div</div>
<script>debugger</script>
<!-- 非常多的内容,让该 html 文件很大 -->
<div>bottom div</div>
</body>
</html>
实际情况为:当开始 debug 时,top div 已经渲染在页面上了 bottom div 还没有渲染出来;
首先肯定可以确定的一个事情:
- 解析是按照顺序,从上至下执行的
- 在整个 DOM 树构建完成之前,会将已经解析的部分渲染出来 (将 DOMContentLoaded 事件作为 dom 树构建完成,document.addEventListener('DOMContentLoaded'))
问题:
- 如果需要 endTag 生成后才开始解析,那么 body 中间的内容呢?此时查看 network 发现只传输了 300Byte; 说明很多的内容都没有传输过来,但是
endTag 都已经确认了,后续的内容传递过来,要怎么确定是 body 的子节点呢?还是直接插到 body 里面? - 如果不是 endTag 生成后开始解析,那么
</body> </html>结束标签, 是浏览器自己创建的吗?会不会创建错误呢?
推测:
我的倾向是第一个,需要 endTag 生成后才开始解析,后续的还未传递过来的内容,应该有某些方法进行定位.
html 变成 token,token 又变成节点,然后浏览器开始构建 DOM 树,大概的流程这样
在构造 DOM 的过程中,将会遇到各种资源:
- 遇到图片时,将会去异步加载这个图片,不会阻塞 html 解析和 DOM 的渲染
- 遇到外联 css 文件时,也会去异步的加载这个 css 文件,加载完成后也会对这个 css 文件进行解析,最后会构建 CSSOM;但是 CSSOM 与 DOM 有一些不同;详细见 CSSOM
- 遇到内联 css 样式时,会直接解析;其他的与外联 css 文件一致
- 遇到普通 script 标签时,将会直接 暂停 html 的解析和 DOM 树的渲染 先执行 js 代码,执行完成后才恢复;因为 js 代码可能会做出 修改节点内容、删除节点等操作;
- 遇到外联的 script 标签,还是与普通 script 一样,暂停解析和渲染,先加载 js 文件,并且等代码执行后才恢复
- 遇到 defer script 标签,将会异步加载 js 文件,并且在触发 DOMContentLoaded 事件之前执行,那么在下载时就不会再阻塞解析了;但是一定会阻塞渲染,因为会在 DOMContentLoaded 触发之前执行。
- 遇到 async script 标签,与 defer 类似,也会异步加载 js 文件,与 defer 的不同之处在于 async script 标签在加载 js 文件后,会立即执行 js 代码,同样执行的代码会阻塞渲染,但是不会阻塞 DOMContentLoaded 事件的触发;也就是说,如果在代码执行前触发 DOMContentLoaded 事件,那么是可以正常执行事件回调函数的。
!注意,不管是 defer 还是 async 标签,在添加了 type="module" 后,表现都发生了变化;可以日后研究
用于阻塞的代码:
let i = 0;
while (true) {
i+=1;
if(i>3000000000) break;
}
alert(123);
构建 CSSOM 树
DOM 中包含了页面的所有内容,那么 CSSOM 包含了页面的所有样式
DOM 的构造是增量的,也就是解析一部分 token 就构建一部分 DOM,但是 CSSOM 不是;因为 css 文件中下面的文件可能会覆盖上面的 css 规则
所以为了避免多次不必要的构建,浏览器在遇到 style 标签时,那么浏览器将会直接解析该 style 标签内所有 css,解析完成后再更新 CSSOM 树;遇到 link 外联的样式表时,将会等到该样式表 加载完成 -> 解析完成 后才更新 CSSOM 树。
CSSOM 的数据结构见参考
CSSOM 树的构建将会阻塞 DOM 渲染;所以 CSSOM 树不会阻塞 html 解析和 DOM 树的构建过程,但是会阻塞 DOM 的渲染过程
CSSOM 树与 DOM 树相似,但是 CSSOM 与 DOM 树是两棵树,是完全独立的。CSSOM 树的构建是非常快的
浏览器应用 CSS 样式,是从右向左进行匹配的,比如说 .link .button 就是先匹配到 .button 然后再匹配到 .link;可能有点反直觉,但是这样其实性能更好,可以直接找到 .button 设置样式; 所以这也是 css 优化的一个方向:避免多层级选择器,避免后代选择器,避免 * 选择器……
还有就是加载外联 css 将会阻塞 js 代码的执行,例如(下面的代码需要开启限速)
<head>
<script>
console.time();
console.timeLog();
document.addEventListener('DOMContentLoaded', function () {
console.log('DOMContentLoaded');
console.timeEnd();
})
</script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.2.3/css/bootstrap-grid.min.css" />
</head>
<body>
<div id="btn" class="text-info">top div</div>
<!--<script>console.log('after link')</script>-->
<script>console.log(getComputedStyle(document.getElementById('btn')).color)</script>
<style>
/* 这里覆盖 bootstrap 的 text-info 但是上面的 script 标签打印的结果并不是 red;因为上面的 script 标签获取的时候 CSSOM 还没有更新呢 */
.text-info { color: red; }
</style>
<div>bottom div</div>
</body>
上面代码的执行结果是:直接打印时间和 DOMContentLoaded;
如果把 <script>console.log('after link')</script> 解除注释,那么将会等到 css 文件加载完成后才打印 DOMContentLoaded;
注意:如果 script 标签内没有一点内容,一个空格都没有,那么也会直接打印 DOMContentLoaded;script 标签内就算是有一个空格,那么也会等到 css 文件加载完成;
这里很好理解,浏览器不知道我们在 script 标签内做了什么,比如说,我需要获取这个 text-info 的 字体颜色;`console.log(getComputedStyle(document.getElementById('btn')).color)` 这个时候应该获取到正确的颜色;但是如果连 css 都还没加载完,那如何获取正确的颜色?
结论:加载 css 文件是否阻塞 js 代码执行(包括 DOMContentLoaded),重点在于 js 代码的位置,如果 js 代码位于 link 标签后,将阻塞 js 代码执行;如果 js 代码放在 link 标签前,那么将不会阻塞;
注意:不要使用 webstorm 的
open in browser因为 webstorm 将会启动一个服务器,并且开启热更新服务,将会导致测试结果发生改变;我就遇到了这样的问题,无论 script 放在那里都会阻塞;直接使用 浏览器 打开文件即可;
构建 render 树
等待 DOM 树和 CSSOM 树构建完成后,就可以开始构建 render 树了;
为了构建 render 树,浏览器大致做了以下事情:
- 从 DOM 树的根开始,遍历每个可见节点。
- 一些节点本身是不可见的,例如
link, meta, script标签等,所以直接省略它们 - 还有一些节点是设置了
display: none;这一些节点也属于不可见节点;但是设置了opacity: 0与visibility: hidden属性的节点,依然属于可见节点;
- 一些节点本身是不可见的,例如
- 对于每个可见节点,通过 CSSOM 匹配对应的规则,并应用。
- 生成所有可见节点,及其内容,及其计算样式的渲染树
所以如果将元素的 display 属性修改为 none 那么一定会引起回流,因为将会重新构建 render 树。
布局(回流,重排)
我们有了渲染树后,知道了所有要渲染出来的东西,那么我们就要开始计算这些东西对应的位置;
这个过程就是布局;
其实这个过程没有什么好说的,大概就是遍历整个渲染树;计算节点应在的位置,精确到像素;
所以如果进行优化要减少回流次数的话
- 会尽量减少引起重新构建 render 树的操作,比如删除元素,修改元素 display 属性为 none
- 要减少可能会影响到布局的操作,比如说元素的宽度高度的改变,字体大小的改变,改变元素位置,改变元素内容,改变窗口大小等
- 某些场景下可以使用 absolute, fixed 定位脱离文档流,这样即使重排,造成的影响也比较小;
- 如果一定会进行造成重排的操作,那么可以将这样的操作集合一起。
注意,如果 <img /> 元素一开始没有指定 width height 属性,那么元素一开始就不会占据位置,将会等到图片加载时,才拥有宽度高度,此时将挤开周围的元素也就引起了「回流」;也会影响用户的体验,属于 cls 问题。
具体的性能优化方案可以看 - 参考第四点。
绘制(重绘)
确定了元素的位置,那么就直接渲染出来了。
因为这一步是在布局的后面,所以重排必定重绘,重绘不一定重排就是这里来的;
重绘:就是浏览器重新将内容绘制一遍,比如,我改变了字体的颜色,那么浏览器就只会重新绘制这一块内容;所以重绘其实并不是很影响性能,所以想要聚焦在重绘这一块提升性能是不够明显的。
具体的性能优化方案可以看 - 参考第四点。

浙公网安备 33010602011771号