引言
在今天的数字化世界,网页加载是一个技术流程,涉及多个步骤。当我们在浏览器中输入网址并按下回车键时,这些请求会经历一系列处理,最终呈现为一个完整的网页。这个过程包括解析网址、查询域名、建立网络连接,以及接收和显示数据。本文将详细介绍这些步骤,帮助读者更好地理解网页是如何从请求到显示的整个过程。
浏览器请求过程
1. 解析URL
浏览器首先解析用户输入的URL,确定协议(如HTTP、HTTPS)、域名、端口(默认为80或404)以及请求的资源路径。
2. DNS查询
如果域名不是本地缓存的一部分,浏览器会进行DNS查询以获取网站服务器的IP地址。这可能包括递归DNS查询,直到找到正确的IP地址。确定双方的IP地址。
3. 建立TCP连接
使用获得的IP地址,浏览器通过TCP/IP协议建立与服务器的连接,确定双方的网络位置。这个过程称为三次握手,确保双方可以安全地发送和接收数据。
三次握手 (Three-way Handshake) 是TCP/IP协议中建立连接的一个过程,用于在客户端和服务器之间创建可靠的通信通道。这个过程确保了双方能够互相接收对方的数据,并且可以确认数据的接收。下面是三次握手的具体步骤:
第一次握手(同步SYN) :
- 客户端向服务器发送一个请求,即SYN(Synchronize)包,其中包含了客户端初始化序列号ISN(Initial Sequence Number)。这表示客户端想要建立一个连接。
第二次握手(同步确认SYN-ACK) :
服务器接收到客户端的SYN包之后,会发送一个SYN-ACK数据包作为响应。这个包包含了服务器的初始化序列号ISN和对客户端SYN的确认ACK(Acknowledgment)。
这说明着服务器已经准备好接收来自客户端的数据。
第三次握手(确认ACK) :
- 客户端收到服务器的SYN-ACK包后,会发送一个ACK包给服务器,确认收到了服务器的SYN。这个包不包含任何数据,只是确认服务器的序列号,并提供客户端自己的下一个序列号。这一步完成后,客户端和服务器都认为连接已经建立。
完成三次握手后,客户端和服务器就可以开始进行数据传输了。这种机制确保了两个方向上的连接都是可用的,并且能够正确地接收对方的确认信号。如果在三次握手过程中任何一个步骤失败,连接将不会建立,避免了无效或错误的连接占用资源。
连接建立后,数据传输结束时,因为TCP是全双工的,即两边都可以同时发送和接收数据,因此在关闭连接时需要从两个方向上分别确认,所以还需要进行四次挥手(Four-way Wave)来断开连接。
4. 发送HTTP/HTTPS请求
一旦TCP连接建立,浏览器将通过该连接发送HTTP或HTTPS请求到服务器。请求通常包含请求行、请求头和请求体。
5. 服务器处理请求
服务器接收到请求后,会解析它,并根据请求的内容生成相应的响应。这可能涉及读取文件、运行脚本、查询数据库等操作。
6. 服务器返回响应
服务器生成响应后,通过TCP连接将其发送回浏览器。响应通常包含状态码(如200 OK表示成功)、响应头和响应体。
7. 浏览器接收响应
浏览器接收到服务器的响应后,开始解析HTML文档。如果HTML文档引用了其他资源(如CSS、JavaScript、图片),浏览器会发起额外的请求来获取这些资源。
8. 渲染页面
随着资源的逐步下载,浏览器开始渲染页面。这个过程可能涉及解析CSS和执行JavaScript代码,从而动态改变页面的布局和功能。
9. 连接关闭
在所有必要的资源都下载完毕后,浏览器可能会关闭与服务器之间的TCP连接,除非服务器和浏览器都支持持久连接(例如HTTP/1.1的keep-alive或HTTP/2的多路复用)。
10. 缓存管理
浏览器会根据服务器响应中的缓存控制指令决定是否缓存某些资源,以便在未来的请求中可以更快地加载页面。
整个过程可能因网络条件、服务器性能、浏览器实现等因素而有所变化。
现代浏览器和Web技术(如HTTP/2和HTTP/3、TLS加密、CDN等)的改进也使这一过程更加高效和安全。
浏览器渲染过程
浏览器的渲染过程是一个复杂且精细的任务,涉及到多个阶段,从接收原始数据到最终在屏幕上呈现网页。下面是详细的步骤:
1.接收数据包并解析HTML
- 浏览器接收到HTTP响应的数据包,这些数据包包含HTML,CSS,JavaScript以及可能的图片和其他资源。
- 数据包中的字节流被解析成字符串,然后进一步解析成HTML标记(Token)。
- 构建DOM树
- 浏览器读取HTML Token,并构建一个DOM树。DOM树是一个表示文档结构的树形数据结构,其中每个节点代表一个HTML。
DOM树大致长这样:
bash
{
tag: 'div',
className: 'title',
id: '',
children: [
{
tag: 'div',
className: 'title',
id: '',
}
]
}
3.加载和解析CSS
- 同时,浏览器会加载CSS文件,并将其解析成CSSOM(CSS Object Model)。CSSOM是一个树状结构,其中包含了样式规则和选择器。
4.构建渲染树
- 浏览器将DOM树和CSSOM树结合起来,创建一个渲染树(render)。渲染树中包含了页面上所有可见的元素及其对应的样式信息。不可见的元素(如display:none)不会出现在渲染树中。
5.布局计算(回流/重排)
- 浏览器计算每个元素在屏幕上的确切位置和尺寸。这个过程称为回流(reflow)。布局计算考虑到了元素的尺寸、位置、边距、填充等属性。
6.GPU绘制(重绘)
- 布局计算完成后,浏览器将渲染树转换为实际的像素,这个过程称为重绘(repaint)。重绘发生在GPU上,以提高效率和性能。
7.合成与显示
- 最后,GPU合成各个图层,将最终的像素呈现到屏幕上。
为什么操作DOM慢?
因为 js引擎线程和渲染线程互斥,所以,当我们通过js来操作DOM的时候,就势必会涉及到两个线程的通信和切换,会带来性能上的损耗
回流
回流(Reflow) ,也称为重排(Layout),是浏览器渲染过程的一部分,它发生在以下几种情况下:
- 页面初次渲染: 当浏览器加载一个页面时,它会构建一个渲染树,该树包含了页面上的所有可见元素以及它们的样式信息。构建渲染树的过程中,浏览器会计算每个元素的位置和大小,这就是初次回流。
- 增加、删除可见的DOM元素: 当DOM树发生变化,如添加或删除可见元素时,浏览器需要重新计算渲染树中受影响部分的布局,以适应新的DOM结构。这通常会导致回流发生。
- 改变元素的几何信息 : 当元素的尺寸、位置或可见性发生改变时,如调整宽度、高度、边距、内边距、变换或设置
display
属性等,浏览器需要重新计算该元素及其周围元素的布局,这也需要回流。 - 窗口大小改变: 当浏览器窗口大小变化时,尤其是当窗口尺寸跨越了某些断点(例如在响应式设计中),布局可能会需要调整以适应新的视口尺寸。这种情况下,浏览器会触发回流以重新计算所有元素的布局。
重绘
重绘(Repainting) 是指当元素的视觉属性发生变化但不影响布局(即几何信息不变)时,浏览器对元素的视觉表现进行更新的过程。
以下是一些触发重绘的常见情况:
-
非几何信息被修改:
- 修改元素的颜色(如背景色、文字颜色)。
- 更改元素的边框样式或颜色。
- 更新元素的背景图像。
- 改变元素的透明度(不涉及尺寸变化)。
- 修改文本内容,只要不会引起文本换行的变化(即不会影响到元素的大小或位置)。
与回流不同,重绘不会重新计算元素的布局位置和大小,只更新其视觉表现。
所以重绘通常会比回流更快,因为它不需要重新布局整个页面或部分页面。
回流必重绘,重绘不一定回流
浏览器的优化
浏览器会维护一个渲染队列,当改变元素的几何属性导致回流发生时,回流行为会被加入到渲染队列中,在达到阈值或者一定时间之后会一次性将渲染队列中所有的回流生效
像下面这段代码,在老版本的浏览器中是会进行四次刷新的,但是在新版本中做了优化,只用回流一次
HTML
div.style.left = '10px'
div.style.top = '10px'
div.style.width = '10px'
div.style.height = '10px'
强制渲染队列刷新
但是在下面的代码中,浏览器就会进行4次回流:
JavaScript
div.style.left = '10px'
console.log(div.offsetLeft);
div.style.top = '10px'
console.log(div.offsetTop);
div.style.width = '10px'
console.log(div.offsetWidth);
div.style.height = '10px'
console.log(div.offsetHeight);
这是因为代码中有导致强制刷新的属性,
像这种能触发强制刷新的属性有以下几种:
offsetTop, offsetLeft, offsetWidth, offsetHeight
(可以读到边框)
clientTop, clientLeft, clientWidth, clientHeight
(读不到边框)
scrollTop, scrollLeft, scrollWidth, scrollHeight
(滚动页面)
JavaScript
let el = document.getElementById('app')
el.style.width = (el.offsetWidth + 1) + 'px'//改变width但不会立马回流
el.style.width = 1 + 'px'
el.offsetWidth
确实会刷新渲染队列,但是此时队列中没有回流行为,所以就算此时会触发刷新队列的行为,也不会产生回流。
第一次el.style.width
改变width但不会立马回流,会进入渲染队列。
第二次el.style.width
进入队列后发现后面没有新的操作要进入到渲染队列中了,才会把队列里的东西清空掉,产生一次回流。
如果是在老版本的浏览器中(老版本中没有渲染队列),会进行两次回流。
减少回流
既然回流会产生性能开销,那我们就需要尽量减少回流。
下面这段代码定义了一个循环,它会执行10,000次。每次迭代时,都会创建一个新的li
元素和一个文本节点,然后将文本节点添加到li
元素中,最后将li
元素添加到ul
元素中。如果直接放在浏览器上运行,它会产生很多次回流,那有什么办法可以减少回流呢?
JavaScript
let ul = document.getElementById("demo");
for (let i = 0; i < 10000; i++) {
let li = document.createElement("li");
let text = document.createTextNode(i)
li.appendChild(text)
ul.appendChild(li);
}
1. display = "none"
让需要修改几何属性的容器先脱离文档流不显示,修改完后再回到文档流中
我们知道渲染树中只显示可见的元素及其对应的样式信息,如果元素属性display = "none"
,那它就不在渲染树里面,也就不会产生回流。
JavaScript
let ul = document.getElementById("demo");
ul.style.display = "none";//不显示
for (let i = 0; i < 10000; i++) {
let li = document.createElement("li");
let text = document.createTextNode(i)
li.appendChild(text)
ul.appendChild(li);
}
ul.style.display = "block";//这样就只会产生一次回流
2.借助文档碎片
-
存放多个子节点而不立即插入到DOM树中
-
在文档碎片内部添加、删除或修改节点不会引起回流
-
所有操作完成后,整个文档碎片可以一次性被添加到DOM树中,这样就只需要一次回流来反映所有变更
JavaScript
let ul = document.getElementById("demo");
let frg = document.createDocumentFragment() // 文档碎片
for (let i = 0; i < 10000; i++) {
let li = document.createElement("li");
let text = document.createTextNode(i)
li.appendChild(text)
frg.appendChild(li);
}
ul.appendChild(frg)
3.克隆节点
通过克隆目标元素并在克隆副本上进行所有的DOM操作,最后替换原始元素。
JavaScript
let ul = document.getElementById("demo");
let clone = ul.cloneNode(true)//创建克隆节点
for (let i = 0; i < 10000; i++) {
let li = document.createElement("li");
let text = document.createTextNode(i)
li.appendChild(text)
clone.appendChild(li);
}
ul.parentNode.replaceChild(clone, ul);//用克隆出来的替换原有的ul
注意
如果
ul
元素有事件监听器或者其他动态绑定的数据,那么在克隆并替换之后,这些绑定将会丢失,除非再次手动地将它们附加到新的元素上
结语
以上就是对浏览器渲染的介绍,希望对你有所帮助,感谢你的阅读!