mirror of
https://github.com/ZiuChen/ZiuChen.github.io.git
synced 2025-08-17 23:19:55 +08:00
286 lines
10 KiB
Markdown
286 lines
10 KiB
Markdown
# 深入理解浏览器运行原理
|
||
|
||
## 网页解析过程
|
||
|
||
输入域名 => DNS解析为IP => 目标服务器返回`index.html`
|
||
|
||
> DNS:Domain Name System
|
||
|
||
## HTML解析过程
|
||
|
||
- 浏览器开始解析`index.html`文件,当遇到`<link>`则向服务器请求下载`.css`文件
|
||
- 遇到`<script>`标签则向服务器请求下载`.js`文件
|
||
|
||
<img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0a90afa7c2534ae78f3eab9200a0095b~tplv-k3u1fbpfcp-watermark.image?" alt="浏览器解析HTML过程" width="70%" />
|
||
|
||
<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/630cf2fd1d3748d6846fe7f2ab99a01b~tplv-k3u1fbpfcp-watermark.image?" alt="浏览器是和如何工作的" width="70%" />
|
||
|
||
[How browsers work](https://web.dev/howbrowserswork/)
|
||
|
||
## 生成CSS规则
|
||
|
||
在解析的过程中,如果遇到`<link>`元素,那么会由浏览器负责下载对应的CSS文件
|
||
|
||
- 注意:下载CSS文件不会影响到DOM解析
|
||
- 有单独一个线程对CSS文件进行下载与解析
|
||
|
||
浏览器下载完CSS文件后,就会对CSS文件进行解析,解析出对应的规则树:
|
||
|
||
- 我们可以称之为CSSOM(CSS Object Model,CSS对象模型)
|
||
|
||
## 构建Render Tree
|
||
|
||
有了DOM Tree和CSSOM Tree之后,就可以将二者结合,构建Render Tree了
|
||
|
||
此时,如果有某些元素的CSS属性`display: none;`那么这个元素就不会出现在Render Tree中
|
||
|
||
- 下载和解析CSS文件时,不会阻塞DOM Tree的构建过程
|
||
- 但会阻塞Render Tree的构建过程:因为需要对应的CSSOM Tree
|
||
|
||
## 布局和绘制(Layout & Paint)
|
||
|
||
第四步是在渲染树(Render Tree)上运行布局(Layout),以计算每个节点的几何体
|
||
|
||
- 渲染树会表示显示哪些节点以及其他的样式,但是不表示每个节点的尺寸、位置等信息
|
||
- 布局是确定呈现树中所有节点的宽度、高度和位置信息
|
||
|
||
第五步是将每个节点绘制(Paint)到屏幕上
|
||
|
||
- 在绘制阶段,浏览器布局阶段计算的每个frame转为屏幕上实际的像素点
|
||
- 包括将元素的可见部分进行绘制,比如文本、颜色、边框、阴影、替换元素
|
||
|
||
## 回流和重绘(Reflow & )
|
||
|
||
回流也可称为重排
|
||
|
||
理解回流(Reflow):
|
||
|
||
- 第一次确定节点的大小和位置,称之为布局(layout)
|
||
- 之后对节点的大小、位置修改重新计算,称之为回流
|
||
|
||
什么情况下会引起回流?
|
||
|
||
- DOM 结构发生改变(添加新的节点或者移除节点)
|
||
- 改变了布局(修改了width height padding font-size等值)
|
||
- 窗口resize(修改了窗口的尺寸等)
|
||
- 调用getComputedStyle方法获取尺寸、位置信息
|
||
|
||
理解重绘(Repaint):
|
||
|
||
- 第一次渲染内容称之为绘制(paint)
|
||
- 之后的重新渲染称之为重绘
|
||
|
||
什么情况下会引起重绘?
|
||
|
||
- 修改背景色、文字颜色、边框颜色、样式等
|
||
|
||
**回流一定会引起重绘,所以回流是一件很消耗性能的事情**
|
||
|
||
- 开发中要尽量避免发生回流
|
||
|
||
- 修改样式尽量一次性修改完毕
|
||
|
||
- 例如通过cssText一次性设置样式,或通过修改class的方式修改样式
|
||
|
||
- 尽量避免频繁的操作DOM
|
||
|
||
- 可以在一个DocumentFragment或者父元素中,将要操作的DOM操作完成,再一次性插入到DOM树中
|
||
|
||
- 尽量避免通过getComputedStyle获取元素尺寸、位置等信息
|
||
|
||
- 对某些元素使用position的absolute或fixed属性
|
||
|
||
- 并不是不会引起回流,而是开销相对较小,不会对其他元素产生影响
|
||
|
||
## 特殊解析: composite合成
|
||
|
||
在绘制的过程中,可以将布局后的元素绘制到多个合成图层中
|
||
|
||
- 这是浏览器的一种优化手段
|
||
- 将不同流生成的不同Layer进行合并
|
||
|
||
```
|
||
标准流 => LayouTree => RenderLayer
|
||
`position:fixed;` => RenderLayer
|
||
```
|
||
|
||
默认情况,标准流中的内容都是被绘制在同一个图层(Layer)中的
|
||
|
||
而一些特殊的属性,浏览器会创建一个新的合成层(CompositingLayer),并且新的图层可以利用GPU来加速绘制
|
||
|
||
- 每个合成层都是单独渲染的
|
||
- 单独渲染可以避免所有的动画都在同一层中渲染导致性能问题
|
||
- 在各自的层中渲染完成后,只需要将渲染结果更新回合成层即可
|
||
|
||
当元素具有哪些属性时,浏览器会为其创建新的合成层呢?
|
||
|
||
- 3D Transforms
|
||
- video canvas iframe
|
||
- opacity 动画转换时
|
||
- position: fixed
|
||
- will-change: 一个实验性的属性,提前告诉浏览器此元素可能发生哪些变化
|
||
- animation 或 transition设置了opacity、transform
|
||
|
||
### 案例1:同一层渲染
|
||
|
||
```
|
||
.box1 {
|
||
width: 100px;
|
||
height: 100px;
|
||
background-color: red;
|
||
}
|
||
.box2 {
|
||
width: 100px;
|
||
height: 100px;
|
||
background-color: blue;
|
||
}
|
||
```
|
||
|
||
```
|
||
<body>
|
||
<div class="box1"></div>
|
||
<div class="box2"></div>
|
||
</body>
|
||
```
|
||
|
||
在开发者工具的图层工具中可以看到,两个元素`.box1` 和 `.box2`都是在一个层(Document)下渲染的:
|
||
|
||
|
||
<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/373fb3a4d5284e6c80c3ec519918e6e4~tplv-k3u1fbpfcp-watermark.image?" alt="image-20221122103111654.png" width="70%" />
|
||
|
||
### 案例2:分层渲染
|
||
|
||
当我们为`.box2`添加上`position: fixed;`属性,这时`.box2`将在由浏览器创建出来的合成层,分层单独渲染
|
||
|
||
```
|
||
.box2 {
|
||
width: 100px;
|
||
height: 100px;
|
||
background-color: blue;
|
||
position: fixed;
|
||
}
|
||
```
|
||
|
||
<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6116d83ee8c041f586d627e549fdd5bf~tplv-k3u1fbpfcp-watermark.image?" alt="image-20221122103256116.png" width="70%" />
|
||
|
||
### 案例3:transform 3D
|
||
|
||
为元素添加上`transform`属性时,浏览器也会为对应元素创建一个合成层,需要注意的是:只有3D的变化浏览器才会创建
|
||
|
||
如果是`translateX`或`translateY`则不会
|
||
|
||
```
|
||
.box2 {
|
||
width: 100px;
|
||
height: 100px;
|
||
background-color: blue;
|
||
/* position: fixed; */
|
||
transform: translateZ(10px);
|
||
}
|
||
```
|
||
|
||
<img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/befb4826b079439f81c98b03586a36e5~tplv-k3u1fbpfcp-watermark.image?" alt="image-20221122103715428.png" width="70%" />
|
||
|
||
### 案例4:transition+transform
|
||
|
||
当我们为元素添加上动画时,动画的中间执行过程的渲染会在新的图层上进行,但是中间动画渲染完成后,结果会回到原始图层上
|
||
|
||
```
|
||
.box2 {
|
||
width: 100px;
|
||
height: 100px;
|
||
background-color: blue;
|
||
transition: transform 0.5s ease;
|
||
}
|
||
.box2:hover {
|
||
transform: translateY(10px);
|
||
}
|
||
```
|
||
|
||
- 这也是使用`transform`执行动画性能更高的原因,因为浏览器会为动画的执行过程单独创建一个合成层
|
||
- 如果是通过修改`top` `left`等定位属性实现的动画,是在原始的图层上渲染完成的。“牵一发则动全身”,动画过程中将导致整个渲染树回流与重绘,极大的影响性能
|
||
|
||
### 案例5:transition+opacity
|
||
|
||
与`transform`类似,使用`transition`过渡的`opacity`动画,浏览器也会为其创建一个合成层
|
||
|
||
```
|
||
.box2 {
|
||
width: 100px;
|
||
height: 100px;
|
||
background-color: blue;
|
||
opacity: 1;
|
||
transition: opacity 0.5s ease;
|
||
}
|
||
.box2:hover {
|
||
opacity: 0.2;
|
||
}
|
||
```
|
||
|
||
### 总结
|
||
|
||
分层确实可以提高性能,但是它是以内存管理为代价的,因此不应当作为Web性能优化策略的一部分过度使用
|
||
|
||
## 浏览器对script元素的处理
|
||
|
||
之前我们说到,在解析到`link`标签时,浏览器会异步下载其中的css文件,并在DOM树构建完成后,将其与CSS Tree合成为RenderTree
|
||
|
||
但是当浏览器解析到`script`标签时,整个解析过程将被阻塞,当前`script`标签后面的DOM树将停止解析,直到当前`script`代码被下载、解析、执行完毕,才会继续解析HTML,构建DOM树
|
||
|
||
为什么要这样做呢?
|
||
|
||
- 这是因为Javascript的作用之一就是操作DOM,并且可以修改DOM
|
||
- 如果我们等到DOM树构建完成并且渲染出来了,再去执行Javascript,会造成回流和重绘,严重影响页面性能
|
||
- 所以当浏览器构建DOM树遇到`script`标签时,会优先下载和执行Javascript代码,而后再继续构建DOM树
|
||
|
||
这也会带来新的问题,比如在现代的页面开发中:
|
||
|
||
- 脚本往往比HTML更“重”,浏览器也需要花更多的时间去处理脚本
|
||
- 会造成页面的解析阻塞,在脚本下载、解析、执行完成之前,用户在界面上什么也看不到
|
||
|
||
为了解决这个问题,浏览器的`script`标签为我们提供了两个属性(attribute):`defer` 和 `async`
|
||
|
||
## defer属性
|
||
|
||
`defer` 即推迟,为`script`标签添加这个属性,相当于告诉浏览器:不要等待此脚本下载,而是继续解析HTML,构建DOM Tree
|
||
|
||
- 脚本将由浏览器进行下载,但是不会阻塞DOM Tree的构建过程
|
||
- 如果脚本提前下载好了,那么它会等待DOM Tree构建完成,在`DOMContentLoaded`**事件触发之前**先执行`defer`中的代码
|
||
|
||
```
|
||
<script>
|
||
console.log('script enter')
|
||
window.addEventListener('DOMContentLoaded', () => {
|
||
console.log('DOMContentLoaded enter')
|
||
})
|
||
</script>
|
||
<script src="./defer.js" defer></script>
|
||
```
|
||
|
||
```
|
||
// defer.js
|
||
console.log('defer script enter')
|
||
```
|
||
|
||
上述代码在控制台的输出为:
|
||
|
||
```
|
||
script enter
|
||
defer script enter
|
||
DOMContentLoaded enter
|
||
```
|
||
|
||
- 多个带`defer`的脚本也是按照自上至下的顺序执行的
|
||
- 从某种角度来说,`defer`可以提高页面的性能,并且推荐放到`head`元素中
|
||
- 注意:`defer`仅适用于外部脚本,对于`script`标签内编写的默认`JS`代码会被忽略掉
|
||
|
||
## async属性
|
||
|
||
`async`属性也可以做到:让脚本异步加载而不阻塞DOM树的构建,它与`defer`的区别:
|
||
|
||
- 用`async`标记的脚本是**完全独立**的
|
||
- `async`脚本不能保证执行顺序,因为它是独立下载、独立运行,不会等待其他脚本
|
||
- 使用`async`标记的脚本不会保证它将在`DOMContentLoaded`之前或之后被执行
|
||
|
||
要使用`async`属性标记的`script`操作DOM,必须在其中使用`DOMContentLoaded`监听器的回调函数,在该事件触发(DOM树构建完毕)后,执行相应的回调函数
|