这两天学习了一些网页如何在浏览器中显示的知识,感觉在我向前端走的路上非常有用,所以就在这里总结一下,大家可以看看,我也算是巩固一下知识。因为自己学识太浅,内容可能有一些错误之处,希望看到的朋友可以指出来啦。
在Edge浏览器也加入Chromium的情况下,我在这里就以现代浏览器来说下浏览器渲染出网页的过程。
下面开始正文:
总括页面加载过程
简述
- 浏览器向服务器发送请求。
- 服务器做出响应,给浏览器返回信息,这些信息就是页面的HTML 文档了。
- 浏览器根据文档,调用浏览器内部程序,将文档转化为我们所看到的样子显示出来。
如果只是想看浏览器加载页面过程的朋友可以直接向下滑到”浏览器渲染页面“的位置,前两部分涉及更多的是计算机网络的知识,感兴趣的可以了解一下。
浏览器发送请求
比如我们在百度或者Google的时候,会在搜索框中输入想要搜索的内容,然后点击搜索,页面就会跳转,并且列出我们可能需要的信息,我们再进行挑选查看。
首先看一下我们网络中使用的TCP/IP模型

TCP/IP模型共有4层,每层都有各自的功能,而且每层都不关心其他各个层的工作,只是做自己本职的工作,同一结点(可以理解为每一个网络站点、服务器、电脑主机等)相邻层之间通过接口通信,不同结点的的相同层之间通过协议通信(如TCP、UDP、HTTP等)。更详细的可以去看看这里,或者可以看看谢希仁的《计算机网络》,非常经典的一本书。
还有一个叫OSI模型,由于这里面不同模型之间关系不是特别适合实际应用(共有7层模型,而且有些不同层又有相同的功能),只是在进行网络设计的时候会参考一下,而且是第一个公认的网络模型。
在TCP/IP协议中,浏览器属于最上层的应用层。当我们鼠标点击搜索或者回车后,就会产生一个事件,这个事件会根据网页代码转化为一个API请求, 其实也会转化成一个文本数据,请求会传递到服务器去查找数据库,从而得到我们想要的东西,而在这其中还要经过TCP/IP的其他所有层才能通过网线传输请求到达服务器。
在应用层使用HTTP协议,HTTP协议根据规则,会打包数据,这时候也会使用HTTPS,来加密数据,防止中途有人篡改我们的数据,保证数据可以准确传输到服务器。打包数据后,应用层会将数据发送到80端口(默认是80端口,也有可能是别的端口),到达传输层。
到达传输层,这里会给数据加一个头部,其中含有源端口号和目的端口号、序号(报文段序号)、校验等等信息。在这一层用到的协议就是TCP协议。
TCP协议是面向连接的,在实际运行中类似与A与B两个人使用微信聊天,A对吧说:”在吗“,B回答说:”在“,两个人就开始聊天了。还有一种协议叫UDP,是面向无连接的,就好比A对B说:”你刚好在楼下,帮我带瓶水上来“,然后B有可能会看到消息,也可能看不到消息。也就是说A与B的通信并没有建立链接。
当传输层完成多有任务后,会将数据传送给下一层的网络层。
网络层 (也叫网际层、互联层)会有一个重要的设备,就是路由器,路由器可以对数据进行存储转发。在这里,我们使用的协议是IP协议 ,IP协议 也会给数据添加一个头部,里面含有源IP地址,和目的IP地址(如192.168.1.1),还有很多其他东西就不说了,最后再加一个尾部,用于定界和帧校验 (如CRC校验) 。这时候就可以把数据传递给最下面的链路层了。
链路层 (也叫物理层,网络层-如果上一层叫互联层的话) 是面向比特流的也就是 011001100001 这种数据,以便通过网线进行长距离传输。在这里基本都是网卡完成的,每张网卡都有一个专属的地址,叫物理地址,也就是我们常说的MAC地址,用这个便可以找到全球任何一台网络设备(嗯?MAC地址克隆,不了,好像法律不允许)。在链路层,也会添加一个头部,其中包含源MAC地址,目的MAC地址等,最后会将整个数据帧转化成二进制比特流传输出去。
经过多个路由器、交换机等,我们的数据就会千里迢迢到达远方的服务器,服务器也会有跟我们电脑类似的网络层级结构,会一层一层将头部、尾部去掉(可能像剥洋葱),得到我们的API请求。
服务器响应请求并回答浏览器
得到请求,服务器会在数据库中检索信息,通过算法对我们的信息加以修饰(度娘的广告?),准备发送到我们的计算机。
回来的路跟我们的数据去的路是类似的,也需要经过传输层、网络层、链路层,最后通过二进制比特流将信息传递给我们的计算机。
计算机会逆序,通过链路层、网络层、传输层的顺序将数据拆开、最后得到的就是我们网页需要显示的HTML文档。
上面都是浏览器向服务器请求数据的网络部分,下面是浏览器得到数据后渲染页面的部分,作为搞前端的,几乎可以不用管上面的部分,那些都是计算机会自己进行的。
浏览器渲染页面
整体过程是这样的:

也就是说,浏览器会将这个文档中的单纯HTML部分、CSS部分以及JS部分分开进行解析、渲染。得到DOM树、 CSS规则树、以及JS对DOM和CSSOM的操作命令。
最后进行融合,将需要立即在页面显示的内容,生成一个 Rendering Tree 渲染树,浏览器便会通过调用浏览器内部的 Native GUI 的 API 根据 Rendering Tree 将各个页面部分“画”到浏览器页面中。
构建DOM
这个过程是这样子的

-
可以看到,得到的字节被转化成字符串,也就是我们所写的HTML结构的样子,
-
然后会解析成一个个的Token标签,类似于下图
pic 浏览器中HTML结构
Token标签中会标识出“开始标签”、“结束标签”、“文本”等等。
- 构建DOM的过程中,会一边解析出Token,一边生成Node节点对象,然后就可以用来渲染页面了,当然这个时候并不会直接渲染的,还要等CSSOM构建完成,搭配JS部分才会完全渲染一个页面的,这个在后面会说到的。
这里举个例子,如下的HTML文档
<html>
<head>
<title> Title </title>
</head>
<body>
<div>
<p>This is a sentence</p>
<a>This is a link</a>
</div>
</body>
</html>
最后会构建一个这样的DOM树

构建CSSOM
构建CSSOM的过程和构建DOM的过程非常相似:

这里在生成结点对象的时候浏览器会匹配DOM结点,我们代码中给标签写的样式才会被加上去。
在匹配结点样式的时候,每一个结点几乎都会不断递归去寻找父节点、祖父结点等等的样式,例如
<html>
<head>
<title>title</title>
<style>
.cdiv{ color: red }
</style>
</head>
<body>
<div class="cdiv">
<p>this is a sentence</p>
</div>
</body>
</html>
页面这样显示

我们看到<p> 标签上并没有定义任何样式,但是渲染的页面中<p> 标签里的文字出现了父级标签定义的样式color: red,这就是在CSSOM渲染中通过递归的方式渲染的结果,而这个过程事实上是比较耗性能的,但是又不能为了这个而将所有样式都进行定义,基本现在的项目都会大致忽略这一性能损耗。
还有的时候,我们的CSS样式会通过过度层叠的方式来写,这个在匹配DOM的时候也会比较耗费性能,所以尽量使用 id 和 class 会非常容易将样式匹配到结点。
构建 Rendering Tree
渲染树的创建是在 DOM 和 CSSOM 全部构建完成之后进行的,这个过程会将两个树结合起来。
以下面这个例子来看
<html>
<head>
<title>title</title>
<link type="" ...>
<style>
.cdiv{color: red;}
.cp{background-color: #dddfff;}
.ca{display: none;}
</style>
</head>
<body>
<div class="cdiv">
<p class="cp">this is a sentence</p>
<a class="ca" href="">this is a link</a>
</div>
</body>
</html>

我们可以看到最后构建的渲染树中没有出现设置为 display: none 的 <a> 标签。所以说,渲染树只会将要显示的结点加进去,或者说,渲染树中有的内容,都是会显示在页面中的内容。
如果代码书写比较规范的话,在这个时候,JS部分就要开始执行了,并且匹配渲染树中的结点。
而事实上,在浏览器中,渲染的过程中,如果遇到<script> 就会停止渲染HTML,也就是阻塞渲染,去执行JS代码,等JS代码执行完,再回到HTML代码继续渲染。这有点类似计算器中的中断机制。(多线程?),在浏览器中,这一步可不能是多线程,因为如果要多线程的话,JS要匹配DOM结点,可能需要进行线程调用或者线程通信,而这样做对性能的损耗会变得更多,更重要的是可能会出现一定的冲突,比如JS已经执行了,但是浏览器还没有渲染到JS操作DOM的结点,这样JS就相当于是无效的。
通常我们JS代码通常会写在<body> 后面,或者JS中用 window.onload(){} 等方法来保证JS在DOM之后执行,从而获取到DOM结点。
当我们的JS文件也需要改变CSS样式的时候,是需要有完整的CSSOM的,那这个时候就不是只有JS会阻塞DOM构建,CSSOM同样会阻塞DOM构建。
过程如下图

这个图中,我们可以看到,在DOM构建过程中,抵达CSS的时候,会和CSS一起进行构建,这时候遇到了JS代码,DOM就需要阻塞,JS执行过程中,遇到需要改变CSS样式的地方,JS也被阻塞,并等待CSSOM构建完成后再去执行JS,JS代码执行结束后,再回到DOM构建。
布局和绘画
经过上面的步骤后,已经生成了一个渲染树,浏览器就会根据这棵树来布局页面(这一步也叫做回流)。我们都知道我们的网页,每一个元素都是“盒子模型”,浏览器会在这个时候弄清楚每一个盒子的绝对位置,包括所有的在代码中以相对定位等生成的元素,这个位置会以像素的形式来布局。
布局完成后,就会输出所有要显示的元素的盒子,然后就是将这些盒子”画“到页面上了,浏览器会通过Paint Setup 和 Paint 事件,将每个像素”画“到屏幕上。
最后,我们再来回顾一下浏览器是如何显示出页面的
- 浏览器通过我们的电脑向服务器发送请求。
- 服务器接收到请求后进行一些算法,给我们的电脑返回一个二进制比特流数据。
- 浏览器将这些数据转化成文本文档。
- 浏览器进行DOM和CSSOM的构建。
- 配合JS代码,形成一个Rendering Tree。
- 浏览器根据Rendering Tree将所有盒子模型转化为像素,在浏览器窗口上绘画出页面。
还有一些小知识点,比如JS的异步加载(async),延迟加载(defer);DOM构建的性能问题;浏览器绘制页面回流和重绘问题;以及以我新手的水平能想到的性能优化策略就放到下一篇在说啦,这篇文章已经有4000字了,估计好多人都没看完呢,哈哈哈哈。
一些没用的话
第一次写这么长的文章,4000字,心里还是非常开心的。
虽然在写这篇文章的时候,我查阅了不少资料,但我知道文中肯定有一些的错误之处,希望大家看到后可以评论留言,让我改正过来,谢谢大家能看到这里,谢谢啦。
参考链接
《计算机网络简明教程》谢希仁
这篇博客详细介绍了网页在浏览器中显示的过程,从浏览器发送请求开始,经过TCP/IP模型的各层,直到服务器响应并返回HTML文档。接着,浏览器解析文档构建DOM和CSSOM,最终形成渲染树并进行布局和绘画。文章强调了JS对渲染的影响,包括阻塞渲染和性能优化策略,适合前端开发者阅读。
1万+





