深入浅出浏览器的渲染过程

3,028 阅读11分钟

作为前端开发者, 我们最熟悉的工具莫过于浏览器, 那么我们有了解过浏览器渲染的过程是什么样子的吗? 接下来让我们来了解一下其渲染的过程吧!

从浏览器的结构开始

从浏览器的结构上来说,浏览器主要是包括了八个子系统:

  1. 界面模块
  2. 浏览器引擎
  3. 渲染引擎
  4. 网络子系统
  5. JavaScript 解释器
  6. XML 解析器
  7. 显示后端
  8. 数据持久性子系统
多进程结构

拿我们就常见的Chrome浏览器举例, Chrome采用了多进程架构,主要是分为四个进程:

  1. 浏览器进程
  2. 插件进程
  3. 渲染进程
  4. GPU进程
Chrome渲染器进程中的线程
  1. GUI渲染线程(负责对浏览器界面进行渲染)
  2. JavaScript引擎线程(负责解析和执行JavaScript脚本)
  3. 浏览器定时器触发线程(setTimeout、setInterval所在的线程)
  4. 浏览器事件触发线程(负责处理浏览器事件,将事件触发后执行的代码放置到JavaScript引擎中执行)
Chrome浏览器进程中的线程
  1. UI线程(用于绘制浏览器的按钮和输入字段)
  2. 网络线程(用于处理网络请求,以及从服务器接收过来的数据)
  3. 存储线程(用于控制文件的访问)

输入url到整个页面完成渲染

DNS解析

一. 在DNS域名解析的过程中,是有涉及到DNS寻找过程的,接着是找到网页资源存放的服务器。

二. 浏览器与服务器建立 TCP 连接(tcp三次握手确立已建立连接,四次挥手断开连接) tcp三次握手包括:

SYN  =>  SYN ACK   => ACK RECEIVED

image.png

  1. 构建请求

  2. 查找缓存,如果有则取缓存

  3. 准备 IP 地址和端口

  4. 等待 TCP 队列

  5. 建立 TCP 连接(TCP三次握手)

这个过程指 TCP 连接的建立过程,该过程中客户端和服务端总共需要发送三个包,从而确认连接存在。

  1. 发送 HTTP 请求

  2. 关闭TCP连接(TCP四次挥手)

四次挥手:指 TCP 连接的断开过程,该过程中需要客户端和服务端总共发送四个包以,从而确认连接关闭。

  1. HTTP响应

当然不知道有没有小伙伴会思考,TCP三次握手哪一次是可以安全携带数据呢?同时也包括TCP的accept发生在三次握手的哪个阶段呢?

1441638670034_.pic.jpg

accept过程发生在三次握手之后,三次握手完成后,客户端和服务器就建立了tcp连接并可以进行数据交互了。

这时可以调用accept函数获得此连接。

connect返回了可以认为连接成功了吗?

connect返回成功后,三次握手就已经完成了。

已完成的链接会被放入一个队列中,accept的作用就是从已连接队列中取出优先级最高的一个链接,并将它绑定给一个新的fd,服务端就可以通过这个新的fd来recv和send数据了。

第三次握手的时候,可以携带。前两次握手不能携带数据。

如果前两次握手能够携带数据,那么一旦有人想攻击服务器,那么他只需要在第一次握手中的 SYN 报文中放大量数据,那么服务器势必会消耗更多的时间和内存空间去处理这些数据,增大了服务器被攻击的风险。

第三次握手的时候,客户端已经处于ESTABLISHED状态,并且已经能够确认服务器的接收、发送能力正常,这个时候相对安全了,可以携带数据。

注意: 客户端最后握手的 ACK 不一定要等到服务端的 HTTP 响应到达才发送,两个过程没有任何关系。

第一次握手时server会计算出cookie传给客户端并缓存,之后的握手客户端会携带cookie进行SYN。

如果cookie不合法直接丢弃,如果合法,就可以直接发送http响应

TCP队头阻塞和HTTP队头阻塞
  1. TCP队头阻塞

很多人认为TCP不存在丢包 UDP是存在丢包的,我个人认为不是这样理解的,因为TCP数据包是有序传输,中间一个数据包丢失,会等待该数据包重传,造成后面的数据包的阻塞。(停止等待)

  1. HTTP队头阻塞

http队头阻塞和TCP队头阻塞完全不是一回事

http1.x采用长连接(Connection:keep-alive),可以在一个TCP请求上,发送多个http请求。

有非管道化和管道化,两种方式。

非管道化,完全串行执行,请求->响应->请求->响应…,后一个请求必须在前一个响应之后发送。

管道化,请求可以并行发出,但是响应必须串行返回。后一个响应必须在前一个响应之后。原因是,没有序号标明顺序,只能串行接收。

管道化请求的致命弱点:

会造成队头阻塞,前一个响应未及时返回,后面的响应被阻塞 请求必须是幂等请求,不能修改资源。因为,意外中断时候,客户端需要把未收到响应的请求重发,非幂等请求,会造成资源破坏。 由于这个原因,目前大部分浏览器和Web服务器,都关闭了管道化,采用非管道化模式。

无论是非管道化还是管道化,都会造成队头阻塞(请求阻塞)。

解决http队头阻塞的方法:

  1. 并发TCP连接(浏览器一个域名采用6-8个TCP连接,并发HTTP请求)

  2. 域名分片(多个域名,可以建立更多的TCP连接,从而提高HTTP请求的并发)

  3. 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):将计算后的信息转换为屏幕上的像素。

  1. 解析

渲染器进程的主线程会解析以下内容:

解析 HTML 内容,产生一个 DOM 节点树;

解析 CSS,产生 CSS 规则树;

解析 Javascript 脚本,由于 Javascript 脚本可以通过 DOM API 和 CSSOM API 来操作 DOM 节点树和 CSS 规则树,因此该过程中会等待 JavaScript 运行完成才继续解析 HTML。

解析完成后,我们得到了 DOM 节点树和 CSS 规则树,布局过程便是通过 DOM 节点树和 CSS 规则树来构造渲染树(Render Tree)。

  1. 布局

通过解析之后,渲染器进程知道每个节点的结构和样式,但如果需要渲染页面,浏览器还需要进行布局,布局过程便是我们常说的渲染树的创建过程。

在这个过程中,像header或display:none的元素,它们会存在 DOM 节点树中,但不会被添加到渲染树里。

布局完成后,将会进入绘制环节。

  1. 绘制

在绘制步骤中,渲染器主线程会遍历渲染树来创建绘制记录。

需要注意的是,如果渲染树发生了改变,则渲染器会触发重绘(Repaint)和重排(回流)(Reflow)。

重绘:屏幕的一部分要重画,比如某个 CSS 的背景色变了,但是元素的几何尺寸没有变。

重排(回流):元素的几何尺寸变了(渲染树的一部分或全部发生了变化),需要重新验证并计算渲染树。

为了不对每个小的变化都进行完整的布局计算,渲染器会将更改的元素和它的子元素进行脏位标记,表示该元素需要重新布局。其中,全局样式更改会触发全局布局,部分样式或元素更改会触发增量布局,增量布局是异步完成的,全局布局则会同步触发。

回流需要涉及变更的所有的结点几何尺寸和位置,成本比重绘的成本高得多的多。所以我们要注意以避免频繁地进行增加、删除、修改 DOM 结点、移动 DOM 的位置、Resize 窗口、滚动等操作,因为这些操作可能会导致性能降低。

  1. 光栅化

通过解析、布局和绘制过程,浏览器获得了文档的结构、每个元素的样式、绘制顺序等信息。将这些信息转换为屏幕上的像素,这个过程被称为光栅化。

光栅化可以被 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连接。

不足之处还请指正~