作为前端开发者, 我们最熟悉的工具莫过于浏览器, 那么我们有了解过浏览器渲染的过程是什么样子的吗? 接下来让我们来了解一下其渲染的过程吧!
从浏览器的结构开始
从浏览器的结构上来说,浏览器主要是包括了八个子系统:
- 界面模块
- 浏览器引擎
- 渲染引擎
- 网络子系统
- JavaScript 解释器
- XML 解析器
- 显示后端
- 数据持久性子系统
多进程结构
拿我们就常见的Chrome浏览器举例, Chrome采用了多进程架构,主要是分为四个进程:
- 浏览器进程
- 插件进程
- 渲染进程
- GPU进程
Chrome渲染器进程中的线程
- GUI渲染线程(负责对浏览器界面进行渲染)
- JavaScript引擎线程(负责解析和执行JavaScript脚本)
- 浏览器定时器触发线程(setTimeout、setInterval所在的线程)
- 浏览器事件触发线程(负责处理浏览器事件,将事件触发后执行的代码放置到JavaScript引擎中执行)
Chrome浏览器进程中的线程
- UI线程(用于绘制浏览器的按钮和输入字段)
- 网络线程(用于处理网络请求,以及从服务器接收过来的数据)
- 存储线程(用于控制文件的访问)
输入url到整个页面完成渲染
DNS解析
一. 在DNS域名解析的过程中,是有涉及到DNS寻找过程的,接着是找到网页资源存放的服务器。
二. 浏览器与服务器建立 TCP 连接(tcp三次握手确立已建立连接,四次挥手断开连接) tcp三次握手包括:
SYN => SYN ACK => ACK RECEIVED
-
构建请求
-
查找缓存,如果有则取缓存
-
准备 IP 地址和端口
-
等待 TCP 队列
-
建立 TCP 连接(TCP三次握手)
这个过程指 TCP 连接的建立过程,该过程中客户端和服务端总共需要发送三个包,从而确认连接存在。
-
发送 HTTP 请求
-
关闭TCP连接(TCP四次挥手)
四次挥手:指 TCP 连接的断开过程,该过程中需要客户端和服务端总共发送四个包以,从而确认连接关闭。
- HTTP响应
当然不知道有没有小伙伴会思考,TCP三次握手哪一次是可以安全携带数据呢?同时也包括TCP的accept发生在三次握手的哪个阶段呢?
accept过程发生在三次握手之后,三次握手完成后,客户端和服务器就建立了tcp连接并可以进行数据交互了。
这时可以调用accept函数获得此连接。
connect返回了可以认为连接成功了吗?
connect返回成功后,三次握手就已经完成了。
已完成的链接会被放入一个队列中,accept的作用就是从已连接队列中取出优先级最高的一个链接,并将它绑定给一个新的fd,服务端就可以通过这个新的fd来recv和send数据了。
第三次握手的时候,可以携带。前两次握手不能携带数据。
如果前两次握手能够携带数据,那么一旦有人想攻击服务器,那么他只需要在第一次握手中的 SYN 报文中放大量数据,那么服务器势必会消耗更多的时间和内存空间去处理这些数据,增大了服务器被攻击的风险。
第三次握手的时候,客户端已经处于ESTABLISHED状态,并且已经能够确认服务器的接收、发送能力正常,这个时候相对安全了,可以携带数据。
注意: 客户端最后握手的 ACK 不一定要等到服务端的 HTTP 响应到达才发送,两个过程没有任何关系。
第一次握手时server会计算出cookie传给客户端并缓存,之后的握手客户端会携带cookie进行SYN。
如果cookie不合法直接丢弃,如果合法,就可以直接发送http响应。
TCP队头阻塞和HTTP队头阻塞
- TCP队头阻塞
很多人认为TCP不存在丢包 UDP是存在丢包的,我个人认为不是这样理解的,因为TCP数据包是有序传输,中间一个数据包丢失,会等待该数据包重传,造成后面的数据包的阻塞。(停止等待)
- HTTP队头阻塞
http队头阻塞和TCP队头阻塞完全不是一回事
http1.x采用长连接(Connection:keep-alive),可以在一个TCP请求上,发送多个http请求。
有非管道化和管道化,两种方式。
非管道化,完全串行执行,请求->响应->请求->响应…,后一个请求必须在前一个响应之后发送。
管道化,请求可以并行发出,但是响应必须串行返回。后一个响应必须在前一个响应之后。原因是,没有序号标明顺序,只能串行接收。
管道化请求的致命弱点:
会造成队头阻塞,前一个响应未及时返回,后面的响应被阻塞 请求必须是幂等请求,不能修改资源。因为,意外中断时候,客户端需要把未收到响应的请求重发,非幂等请求,会造成资源破坏。 由于这个原因,目前大部分浏览器和Web服务器,都关闭了管道化,采用非管道化模式。
无论是非管道化还是管道化,都会造成队头阻塞(请求阻塞)。
解决http队头阻塞的方法:
-
并发TCP连接(浏览器一个域名采用6-8个TCP连接,并发HTTP请求)
-
域名分片(多个域名,可以建立更多的TCP连接,从而提高HTTP请求的并发)
-
HTTP2方式
http2使用一个域名单一TCP连接发送请求,请求包被二进制分帧**(多路复用)**不同请求可以互相穿插,避免了http层面的请求队头阻塞。但是不能避免TCP层面的队头阻塞。
UDP
UDP如何实现可靠传输?
传输层无法保证数据的可靠传输,只能通过应用层来实现了。
实现的方式可以参照tcp可靠性传输的方式,只是实现不在传输层,实现转移到了应用层。
最简单的方式是在应用层模仿传输层TCP的可靠性传输。
下面不考虑拥塞处理,可靠UDP的简单设计。
1、添加seq/ack机制,确保数据发送到对端
2、添加发送和接收缓冲区,主要是用户超时重传。
3、添加超时重传机制。
详细说明:送端发送数据时,生成一个随机seq=x,然后每一片按照数据大小分配seq。数据到达接收端后接收端放入缓存,并发送一个ack=x的包,表示对方已经收到了数据。发送端收到了ack包后,删除缓冲区对应的数据。时间到后,定时任务检查是否需要重传数据。 解析(Parser):解析 HTML/CSS/JavaScript 代码。
实际上,在前端性能优化当中,网络请求的优化往往占据了很大一部分,包括首屏直出、分包加载、数据分片拉取、使用缓存、预加载等,都是通过合理地减少网络请求内容、减少网络请求的等待耗时等方式,达到预期的优化效果。
三. 布局
布局(Layout):定位坐标和大小、是否换行、各种position/overflow/z-index属性等计算。
绘制(Paint):判断元素渲染层级顺序。
光栅化(Raster):将计算后的信息转换为屏幕上的像素。
- 解析
渲染器进程的主线程会解析以下内容:
解析 HTML 内容,产生一个 DOM 节点树;
解析 CSS,产生 CSS 规则树;
解析 Javascript 脚本,由于 Javascript 脚本可以通过 DOM API 和 CSSOM API 来操作 DOM 节点树和 CSS 规则树,因此该过程中会等待 JavaScript 运行完成才继续解析 HTML。
解析完成后,我们得到了 DOM 节点树和 CSS 规则树,布局过程便是通过 DOM 节点树和 CSS 规则树来构造渲染树(Render Tree)。
- 布局
通过解析之后,渲染器进程知道每个节点的结构和样式,但如果需要渲染页面,浏览器还需要进行布局,布局过程便是我们常说的渲染树的创建过程。
在这个过程中,像header或display:none的元素,它们会存在 DOM 节点树中,但不会被添加到渲染树里。
布局完成后,将会进入绘制环节。
- 绘制
在绘制步骤中,渲染器主线程会遍历渲染树来创建绘制记录。
需要注意的是,如果渲染树发生了改变,则渲染器会触发重绘(Repaint)和重排(回流)(Reflow)。
重绘:屏幕的一部分要重画,比如某个 CSS 的背景色变了,但是元素的几何尺寸没有变。
重排(回流):元素的几何尺寸变了(渲染树的一部分或全部发生了变化),需要重新验证并计算渲染树。
为了不对每个小的变化都进行完整的布局计算,渲染器会将更改的元素和它的子元素进行脏位标记,表示该元素需要重新布局。其中,全局样式更改会触发全局布局,部分样式或元素更改会触发增量布局,增量布局是异步完成的,全局布局则会同步触发。
回流需要涉及变更的所有的结点几何尺寸和位置,成本比重绘的成本高得多的多。所以我们要注意以避免频繁地进行增加、删除、修改 DOM 结点、移动 DOM 的位置、Resize 窗口、滚动等操作,因为这些操作可能会导致性能降低。
- 光栅化
通过解析、布局和绘制过程,浏览器获得了文档的结构、每个元素的样式、绘制顺序等信息。将这些信息转换为屏幕上的像素,这个过程被称为光栅化。
光栅化可以被 GPU 加速,光栅化后的位图会被存储在 GPU 内存中。根据前面介绍的渲染流程,当页面布局变更了会触发重排和重绘,还需要重新进行光栅化。此时如果页面中有动画,则主线程中过多的计算任务很可能会影响动画的性能。
因此,现代的浏览器通常使用合成的方式,将页面的各个部分分成若干层,分别对其进行栅格化(将它们分割成了不同的瓦片),并通过合成器线程进行页面的合成。
合成过程如下:
当主线程创建了合成层并确定了绘制顺序,便将这些信息提交给合成线程;
合成器线程将每个图层栅格化,然后将每个图块发送给光栅线程;
光栅线程栅格化每个瓦片,并将它们存储在 GPU 内存中;
合成器线程通过 IPC 提交给浏览器进程,这些合成器帧被发送到 GPU 进程处理,并显示在屏幕上。
合成的真正目的是,在移动合成层的时候不用重新光栅化。因为有了合成器线程,页面才可以独立于主线程进行流畅的滚动。
到这里,页面才真正渲染到屏幕上。
在绘制页面的时候,也可能会遇到很多奇怪的渲染问题,比如使用了transform:scale可能会导致某些浏览器中渲染模糊,究其原因则是由于光栅化过程导致的。
总结
1. DNS解析
2. TCP连接
3. 发送HTTP请求
4. 服务器处理请求并返回需要的数据
5. 浏览器解析渲染页面
6. 连接结束
输入域名,域名要通过DNS解析找到这个域名对应的服务器地址(ip),通过TCP请求链接服务,通过WEB服务器(apache)返回数据,浏览器根据返回数据构建DOM树,通过css渲染引擎及js解析引擎将页面渲染出来,关闭tcp连接。
不足之处还请指正~