# 渲染面板
关于页面渲染方面的性能,我们可以在 Chorme 开发者工具中的渲染面板 Rendering 中观察到。
其中,FPS meter 是我们最需要关注的一个点。FPS 全称叫 Frames Per Second (每秒帧数)。帧数越高,动画显示的越流畅。一般的液晶显示器的刷新频率也就是 60 HZ。也就是说,要想页面上的交互效果和动画流畅,那么 FPS 稳定在 60 左右,是最佳的体验。
关于 Chorme 开发工具各个面板的具体使用可以参考这个:Chrome开发者工具使用指南 (opens new window)
# 渲染性能测试
重排(回流)一定会引起重绘,但重绘不一定会引起重排(回流)。
(1)不同的动画实现方式会导致渲染性能不一样
<div class="container">
<div class="ball" id="ball"></div>
</div>
<script>
var balls = document.getElementById('ball');
balls.classList.add('ball');
balls.classList.add('ball-running');
</script>
2
3
4
5
6
7
8
- 使用改变手动改变 top 和 left 的方式实现。
.container {
position: relative;
min-height: 400px;
}
.ball {
position: absolute;
top: 0;
left: 0;
width: 100px;
height: 100px;
border-radius: 50%;
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.75);
}
.ball-running {
animation: run-around 4s infinite;
}
@keyframes run-around {
0% {
top: 0;
left: 0;
}
25% {
top: 0;
left: 200px;
}
50% {
top: 200px;
left: 200px;
}
75% {
top: 200px;
left: 0;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
可以看到,小球每次在运动的时候,都会导致重绘和重排(绿色高亮部分是重绘,紫色高亮部分是重排),这样会导致页面的渲染性能比较差。
- 使用 translate 的方式来实现。
.container {
position: relative;
min-height: 400px;
}
.ball {
position: absolute;
top: 0;
left: 0;
width: 100px;
height: 100px;
border-radius: 50%;
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.75);
}
.ball-running {
animation: run-around 4s infinite;
}
@keyframes run-around {
0% {
transform: translate(0, 0);
}
25% {
transform: translate(200px, 0);
}
50% {
transform: translate(200px, 200px);
}
75% {
transform: translate(0, 200px);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
可以看到,现在小球每次运动时,不会有高亮显示了,说明不会触发重绘和重排,页面渲染性能得到了提升。
# 网页渲染流程
# 浏览器解析 Html ⽂件的过程
浏览器解析 HTML ⽂件的过程是⼀个复杂且精细的操作,通常包括以下⼏个主要步骤:
1. 下载 HTML ⽂件
当你访问⼀个⽹⻚时,浏览器⾸先向服务器发送请求,下载 HTML ⽂件。
2. 解析 HTML 到 DOM
解析标记(Lexical Analysis):浏览器开始解析 HTML ⽂件中的⽂本,并将其转换成有意义的标记(tokens)。这个过程类似于语法分析。
构建 DOM 树:浏览器根据这些标记构建⽂档对象模型(DOM),它是⼀个树形结构,代表了⻚⾯的内容和结构。
3. 处理 CSS 和 JavaScript
CSS 解析:浏览器解析与 HTML ⽂件相关联的 CSS ⽂件,并根据这些样式信息创建 CSSOM(CSS 对象模型)。
JavaScript 执⾏:如果 HTML ⽂件中包含或引⽤了 JavaScript,浏览器也会解析并执⾏它。JavaScript 可以修改 DOM 和 CSSOM。
4. 构建渲染树
浏览器结合 DOM 和 CSSOM 构建渲染树(Render Tree)。渲染树是⻚⾯中所有可⻅元素的表示,包括它们的位置和样式信息。
5. 布局(Layout/Reflow)
浏览器计算渲染树中每个节点的确切位置和⼤⼩。这个过程也被称为回流(reflow)。
6. 绘制(Painting)
浏览器根据布局信息在屏幕上绘制所有的元素。这包括⽂本颜⾊、图像、边框等。
7. 合成(Compositing)
在某些情况下,⻚⾯的某些部分可能需要单独绘制,然后将它们合并到⼀起。这通常⽤于复杂的动画和视觉效果。
# 从输⼊ URL 到⻚⾯出现的全过程
1. DNS 解析
查找 IP 地址:浏览器需要找到该 URL 的 IP 地址。⾸先在本地缓存中查找,如果未找到,会向 ISP 的 DNS 服务器发起查询。
递归查询:如果 ISP 的 DNS 服务器也没有缓存,则进⾏递归查询,直到找到域名的授权 DNS 服务器,并获取到⽹站的 IP 地址。
2. 建⽴ TCP 连接
- 三次握⼿:浏览器与服务器之间建⽴ TCP 连接,这通常涉及⼀个称为 “三次握⼿” 的过程,确保双⽅都准备好进⾏数据传输。
3. TLS 握⼿(如果是 HTTPS)
- 建⽴安全连接:如果 URL 是 HTTPS,浏览器与服务器之间会进⾏ TLS 握⼿,以建⽴加密的数据传输通道。
4. 发送 HTTP 请求
- 构建请求:浏览器构建⼀个 HTTP 请求,并通过建⽴的 TCP 连接发送给服务器。这个请求包括 URL、请求⽅法(如 GET)、头信息等。
5. 服务器处理请求
- 接收和处理:服务器接收到请求后,会根据请求的 URL 处理请求,可能涉及查询数据库、运⾏后端代码等操作。
6. 服务器发送响应
- 发送数据:服务器向浏览器发送⼀个 HTTP 响应,包括状态码(如 200 OK)、响应头信息、以及请求的⽂档(通常是 HTML)。
7. 浏览器解析 HTML
构建 DOM 树:浏览器开始解析 HTML ⽂档,构建⽂档对象模型(DOM)。
CSS 解析:同时解析 CSS,构建 CSS 对象模型(CSSOM)。
JavaScript 处理:如果 HTML 引⽤了 JavaScript ⽂件,浏览器会下载并执⾏它们,这可能会修改 DOM 和 CSSOM。
8. 渲染⻚⾯
⽣成渲染树:浏览器结合 DOM 和 CSSOM ⽣成渲染树。
布局:计算出渲染树中每个节点的位置和⼤⼩。
绘制:根据布局信息在屏幕上绘制出⻚⾯内容。
9. 加载额外资源
- ⻚⾯中可能包含图⽚、CSS ⽂件、JavaScript ⽂件等额外资源,浏览器会发出额外的 HTTP 请求来加载这些资源。
10. ⻚⾯渲染完成
- 当所有资源都被加载和渲染后,⻚⾯加载过程完成。
在整个过程中,浏览器和服务器之间可能会有多次往返通信。此外,现代⽹⻚可能包含⼤量的资源和复杂的脚本,这些都可能影响加载时间。每⼀步的性能和效率对于最终⽤户体验都⾄关重要。
# 浏览器的渲染流程
浏览器的渲染流程是⼀个包含多个步骤的复杂过程,主要涉及以下环节:
1. 解析 HTML 构建 DOM 树
浏览器解析 HTML ⽂件,创建 DOM 树。
2. 解析 CSS 构建 CSSOM 树
解析 CSS ⽂件和样式,构建 CSS 对象模型(CSSOM)树。
3. 合并 DOM 和 CSSOM 构建渲染树
将 DOM 和 CSSOM 合并,创建渲染树,这⼀步确定每个节点的样式。
4. 布局(Layout)
计算渲染树中每个节点的准确位置和⼤⼩。
5. 分层(Layering)
⼀些元素(如具有复杂效果的元素,使⽤ 3D 变换或 will-change 的元素)会被分到单独的合成层。
6. 绘制(Paint)
在各个层上填充像素,包括颜⾊、图⽚、⽂字等。
7. 合成(Compositing)
分层之后,在屏幕显示之前,浏览器会将所有层合并。这个过程通常由 GPU 加速,特别是当使⽤ CSS3D 变换等技术时。
# 重排和重绘
1. 重排(Reflow)
当 DOM 的变化影响元素的⼏何属性(如宽度、⾼度、位置等),浏览器需要重新计算元素的位置和⼤⼩。这可能导致整个⻚⾯或⻚⾯的⼀部分重新布局。
2. 重绘(Repaint)
当元素的外观被改变,但不影响其布局时(如更改颜⾊、阴影等),浏览器会重新绘制这些元素。
# 什么情况下会导致重绘重排?
颜色、阴影、字体大小等的变化都会导致重绘。
只要盒子动了,就会产生重排。除此之外,在读取一些属性的时候也会触发重排,这些属性有 offset、scroll、client、width 等。因此,平时在写 css 的时候,尽量把读操作放在一起,把写操作放在一起,不要读写读写,这样浏览器会进行优化。
requestAnimationFrame (opens new window) 是操作 DOM 元素的一种高效的方法。我们可以使用它设置读写分离。
requestAnimationFrame(function() {
// 设置读写分离
})
2
3
CSS Triggers (opens new window) 这个网站可以查看 css 属性会不会引起重绘和重排。
fastdom (opens new window) 可以帮助我们更好的管理 DOM 元素的读写操作。
Jank Free (opens new window) 上有很多关于浏览器渲染绘制动画相关的文章和视频可以学习,不过都是英文的。
# 如何避免重排和重绘
1. 最⼩化DOM操作
使⽤ DocumentFragment (opens new window) 或隐藏元素进⾏批量修改。
2. 批量修改样式
批量读取 CSS 样式让浏览器进⾏合并,然后使⽤ rerequestIdleCallback (opens new window) 批量修改,即读取和修改分离。
3. 使⽤ transform 和 opacity 实现动画
这些属性不会触发重排和重绘。
4. 避免触发同步布局事件
避免频繁读取会触发重排的属性。
5. 使⽤绝对定位
对于频繁重排的元素,使⽤绝对定位。
6. 优化 CSS 选择器
优化 CSS 选择器,避免过度复杂的选择器。浏览器解析 CSS 选择器是从右向左的,因此选择器的最右边是关键。
7. 避免使⽤表格布局
因为它可能导致更多的重排。
8. 使⽤ CSS 3D Transform
触发硬件加速,将元素提升到单独的合成层。
9. 利⽤ will-change 属性
will-change (opens new window) 可以提前通知浏览器元素可能的变化,以便优化。
# DOM 分层
DOM 是分层的。那么是如何分层的呢?
(1)浏览器获取到 DOM 元素。
(2)对 DOM 元素节点计算样式结果(Recalculate Style 样式重计算)。
(3)为每个节点生成图形位置(Layout 重排)。
(4)将每个节点绘制填充到图层位图中(Paint 重绘)。
(5)将图层作为纹理上传到 GPU 上去。
(6)GPU 把符合的图层合成生成到页面上(Composite Layers 合成层)。
以上就是整个网页的渲染流程,其中最重要三步就是:Layout -> Paint -> Composite Layers。
可以在 Chorme 开发者工具的性能面板中查看具体的过程和耗时。
# 合成层做了什么?
(1)图层列表准备好之后,提交给主线程(单独的合成线程)。
(2)合成线程根据当前视口 viewport 来划分图块。
(3)将分成的图块生成位图,这个过程就叫光栅化或栅格化(Raster)。
(4)所有图块都栅格化之后,合成线程就会生成 DrawQuad,提交给浏览器的渲染进程。
(5)浏览器有一个 viz 组件,接收到 DrawQuad 之后,把内容绘制到屏幕上。
# 哪些元素会分层?
会分层的元素有:根元素,设置了 position、transform、半透明、css 滤镜、overflow 的元素,canvas 元素,video 元素。
CSS3D、transform、video、WebGL、css 滤镜、will-change: transform (opens new window) 能够跳过重绘重排,让 GPU 直接参与进来,触发硬件加速。
# CPU 和 GPU 的区别
CPU 需要很强的通用性来处理各种不同的数据类型,同时又要逻辑判断又会引入大量的分支跳转和中断的处理。因此它不仅被 Cache 占据了大量空间,而且还有有复杂的控制逻辑和诸多优化电路,相比之下计算能力只是 CPU 很小的一部分。
GPU 面对的则是类型高度统一的、相互无依赖的大规模数据和不需要被打断的纯净的计算环境。它采用了数量众多的计算单元和超长的流水线,但只有非常简单的控制逻辑并省去了 Cache。
适合在 GPU 上运行的程序
计算密集型的程序。所谓计算密集型(Compute-intensive)的程序,就是其大部分运行时间花在了寄存器运算上,寄存器的速度和处理器的速度相当,从寄存器读写数据几乎没有延时。可以做一下对比,读内存的延迟大概是几百个时钟周期;读硬盘的速度就不说了,即便是 SSD, 也实在是太慢了。
易于并行的程序。GPU 其实是一种 SIMD(Single Instruction Multiple Data)架构,它有成百上千个核,每一个核在同一时间最好能做同样的事情。
有一个库叫 GPU.js (opens new window),可以用来加速 JavaScript。
关于两者的详细区别可以参考这篇文章:CPU 和 GPU 的区别 (opens new window)。
# JS 代码放到底部会阻塞 DOM 渲染吗?
测试代码:
<h1>深圳</h1>
<script>
prompt('等待');
</script>
2
3
4
上面代码在浏览器中的效果如下:
可以看到,即使我们把 js 代码放到了页面底部,页面还是会先弹出等待框,此时页面上也并没有显示出 h1 的内容。只有当我们输入内容点确定之后,页面上才会显示 h1 的内容。
因此,不管我们把 js 代码放到页面顶部还是底部,它都会影响 DOM 渲染。
但是,把 js 放页面底部还是有意义的,因为这样不会影响 DOM 解析,只会影响 DOM 渲染。
# CSS 会影响 DOM 的解析和渲染吗?
测试代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
h1 {
color: red !important;
}
</style>
<script>
function h() {
console.log(document.querySelectorAll('h1'));
}
setTimeout(h, 0);
</script>
<link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/5.0.0-alpha1/css/bootstrap-grid.css">
</head>
<body>
<h1>深圳</h1>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
然后需要在浏览器的 Network 那里新增一个网络,来模拟网速很慢的情况,才看得出加载效果。
接着在浏览器中访问页面,
如果控制台中有打印出节点信息,说明 css 不会影响 DOM 解析;否则会影响。
如果页面中会马上显示出 h1 的内容,说明 css 不会影响 DOM 渲染;否则会影响。
可以看到效果如下,控制台输出节点信息了,但是页面上要等很久才会显示 h1 的内容。
因此,css 不会影响 DOM 解析,但是会影响 DOM 渲染。
# CSS 会影响 JS 的解析吗?
测试代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
h1 {
color: red !important;
}
</style>
<link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/5.0.0-alpha1/css/bootstrap-reboot.css">
</head>
<body>
<h1>深圳</h1>
<script>
console.log(123);
</script>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
还是采用刚刚很慢的网络,可以在浏览器中看到,js 代码等了很久之后才执行。
因此,css 会阻塞 js 代码的解析。
# CSS 会影响 DOMContentLoaded 吗?
DOMContentLoaded (opens new window)
测试代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script>
document.addEventListener('DOMContentLoaded', function() {
console.log('DOMContentLoaded');
})
</script>
<link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/5.0.0-alpha1/css/bootstrap-utilities.css">
</head>
<body>
<h1>深圳</h1>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
网速很慢的情况下执行结果如下:
说明此时 css 不会影响 DOMContentLoaded。
但是,在以下情况,就会影响了。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script>
document.addEventListener('DOMContentLoaded', function() {
console.log('DOMContentLoaded');
})
</script>
<link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/5.0.0-alpha1/css/bootstrap-utilities.css">
<script>
console.log('下一个 js');
</script>
</head>
<body>
<h1>深圳</h1>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
网速很慢的情况下执行结果如下:
可以看到,控制台没有输出内容了。说明此时 css 影响了 DOMContentLoaded。当 css 加载完成之后,才会输出内容。
因此,css 会不会影响 DOMContentLoaded 取决于 css 下边还有没有 js 脚本,如果有,就会影响,没有的话就不会影响。
为什么有脚本就会阻塞呢?
这是因为脚本可以操作 DOM,加载 CSS 的时候不知道下面的脚本会不会影响 DOM,所以就要一直等着。