一文摸透从输入URL到页面渲染的过程

一文摸透从输入 URL 到页面渲染的过程

从输入 URL 到页面渲染需要 Chrome 浏览器的多个进程配合,所以我们先来谈谈现阶段 Chrome 浏览器的多进程架构。

一、 Chrome 架构

目前 Chrome 采用的是多进程的架构模式,可分为主要的五类进程,分别是:浏览器( Browser )主进程、 GPU 进程、网络( NetWork )进程、多个渲染进程和多个插件进程;

  • 浏览器进程 。主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
  • 渲染进程 。核心任务是将 HTMLCSSJavaScript 转换为用户可以与之交互的网页,排版引擎 BlinkJavaScript 引擎 V8 都是运行在该进程中,默认情况下, Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。
  • GPU 进程 。其实, Chrome 刚开始发布的时候是没有 GPU 进程的。而 GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、 ChromeUI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后, Chrome 在其多进程架构上也引入了 GPU 进程。
  • 网络进程 。主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。
  • 插件进程 。主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响

了解了 Chrome 的多进程架构,就能够从宏观上理解从输入 URL 到页面渲染的过程了,这个过程主要分为 导航阶段渲染阶段

二、导航阶段

Ⅰ.浏览器主进程

1.用户输入 URL

  • 1、 浏览器进程检查 url ,组装协议,构成完整的 url ,这时候有两种情况:
    • 输入的是搜索内容:地址栏会使用浏览器默认的搜索引擎,来合成新的带搜索关键字的 URL
    • 输入的是请求 URL :地址栏会根据规则,给这段内容加上协议,合成为完整的 URL
  • 2、 浏览器进程通过进程间通信( IPC )把 url 请求发送给网络进程;

Ⅱ.网络进程

2. URL 请求过程

  • 3、 网络进程接收到 url 请求后检查本地缓存是否缓存了该请求资源,如果有则将该资源返回给浏览器进程;

这里涉及到浏览器的缓存策略问题,有兴趣的可以上网查阅相关资料。

  • 4、准备 IP 地址和端口:进行 DNS 解析时先查找缓存,没有再使用 DNS 服务器解析,查找顺序为:

    • 浏览器缓存;
    • 本机缓存;
    • hosts 文件;
    • 路由器缓存;
    • ISP DNS 缓存;
    • DNS 递归查询(本地 DNS 服务器 -> 权限 DNS 服务器 -> 顶级 DNS 服务器 -> 13 台根 DNS 服务器)
  • 5、等待 TCP 队列:浏览器会为每个域名最多维护 6TCP 连接,如果发起一个 HTTP 请求时,这 6TCP 连接都处于忙碌状态,那么这个请求就会处于排队状态;解决方案:

    • 采用域名分片技术:将一个站点的资源放在多个( CDN )域名下面。
    • 升级为 HTTP2 ,就没有 6TCP 连接的限制了;
  • 6、通过三次握手建立 TCP 连接:

    • 第一次: 客户端先向服务器端发送一个同步数据包,报文的 TCP 首部中:标志位: 同步 SYN 1 ,表示这是一个请求建立连接的数据包;序号 Seq=xx 为所传送数据的第一个字节的序号,随后进入 SYN-SENT 状态;

    标志位值为 1 表示该标志位有效。

    • 第二次: 服务器根据收到数据包的 SYN 标志位判断为建立连接的请求,随后返回一个确认数据包,其中标志位 SYN=1ACK=1 ,序号 seq=y ,确认号 ack=x + 1 表示收到了客户端传输过来的 x 字节数据,并希望下次从 x+1 个字节开始传,并进入 SYN-RCVD 状态;

    这里要区分标志位 ACK 和确认号 ack

    • 第三次: 客户端收到后,再给服务器发送一个确认数据包,标志位 ACK=1 ,序号 seq=x+1 ,确认号 ack=y+1 ,随后进入 ESTABLISHED 状态;

    服务器端收到后,也进入 ESTABLISHED 状态,由此成功建立了 TCP 连接,可以开始数据传送;

    • 为什么要第三次挥手? 避免服务器等待造成 资源浪费 ,具体原因:

    如果没有最后一个数据包确认(第三次握手), A 先发出一个建立连接的请求数据包,由于网络原因绕远路了。 A 经过设定的超时时间后还未收到 B 的确认数据包。

    于是发出第二个建立连接的请求数据包,这次网路通畅, B 的确认数据包也很快就到达 A 。于是 AB 开始传输数据;

    过了一会 A 第一次发出的建立连接的请求数据包到达了 BB 以为是再次建立连接,所以又发出一个确认数据包。由于A已经收到了一个确认数据包,所以会忽略 B 发来的第二个确认数据包,但是 B 发出确认数据包之后就要一直等待 A 的回复,而 A 永远也不会回复。

    由此造成服务器资源浪费,这种情况多了 B 计算机可能就停止响应了。

  • 7、构建并发送 HTTP 请求信息;

  • 8、服务器端处理请求;

  • 9、客户端处理响应,首先检查服务器响应报文的状态码:

    • 如果是 301/302 表示服务器已更换域名需要重定向,这时网络进程会从响应头的 Location 字段里面读取重定向的地址,然后再发起新的 HTTP 或者 HTTPS 请求,跳回第 4 步。
    • 如果是 200 ,就检查 Content-Type 字段,值为 text/html 说明是 HTML 文档,是 application/octet-stream 说明是文件下载;

  • 10、 请求结束,当通用首部字段 Conection 不是 Keep-Alive 时,即不为 TCP 长连接时,通过四次挥手断开 TCP 连接:

  • 第一次: 客户端(主动断开连接)发送数据包给服务器,其中标志位 FIN=1 ,序号位 seq=u ,并停止发送数据;
  • 第二次: 服务器收到数据包后,由于还需传输数据,无法立即关闭连接,先返回一个标志位 ACK=1 ,序号 seq=v ,确认号 ack=u+1 的数据包;
  • 第三次: 服务器准备好断开连接后,返回一个数据包,其中标志位 FIN=1 ,标志位 ACK=1 ,序号 seq=w ,确认号 ack=u+1
  • 第四次: 客户端收到数据包后,返回一个标志位 ACK=1 ,序号 seq=u+1 ,确认号 ack=w+1 的数据包。

由此通过四次挥手断开 TCP 连接。

详细过程参见: 详解TCP连接的“三次握手”与“四次挥手”(上)

  • 为什么要四次挥手? 由于服务器不能马上断开连接,导致 FIN 释放连接报文与 ACK 确认接收报文需要分两次传输,即第二次和第三次”挥手”;

3.准备渲染进程

  • 11、 准备渲染进程:浏览器进程检查当前 url 是否与之前打开了渲染进程的页面的根域名相同,如果相同,则复用原来的进程,如果不同,则开启新的渲染进程;

4.提交文档

  • 12、 提交文档:
    • 渲染进程 准备好后, 浏览器渲染进程 发起“ 提交文档 ”的消息, 渲染进程 接收到消息后与 网络进程 建立传输数据的“ 管道
    • 渲染进程 接收完数据后,向浏览器发送“ 确认提交
    • 浏览器进程 接收到确认消息后更新浏览器界面状态: 安全状态 地址栏 url 前进后退的历史状态 更新 web 页面

三、渲染阶段

在渲染阶段通过 渲染流水线 在渲染进程的主线程和合成线程配合下,完成页面的渲染;

Ⅲ.渲染进程

渲染进程中的主线程部分

5.构建 DOM

  • 13、先将请求回来的数据解压,随后 HTML 解析器将其中的 HTML 字节流 通过 分词器 拆分为一个个 Token ,然后生成节点 Node ,最后解析成浏览器识别的 DOM 树结构。

    可以通过 Chrome 调试工具的 Console 选项打开控制台输入 document 查看 DOM 树;

渲染引擎还有一个 安全检查模块XSSAuditor ,是用来 检测词法安全 的。在分词器解析出来 Token 之后,它会检测这些模块是否安全,比如 是否引用了外部脚本 是否符合 CSP 规范 是否存在跨站点请求 等。如果出现不符合规范的内容, XSSAuditor 会对该脚本或者下载任务 进行拦截

首次解析 HTML渲染进程 会开启一个 预解析线程 ,遇到 HTML 文档中内嵌的 JavaScriptCSS 外部引用就会同步提前下载这些文件,下载时间以最后下载完的文件为准。

6.构建 CSSOM

  • 14、 CSS 解析器将 CSS 转换为浏览器能识别的 styleSheets 也就是 CSSOM :可以通过控制台输入 document.styleSheets 查看;

    这里要考虑一下阻塞的问题,由于 JavaScript 有修改 CSSHTML 的能力,所以,需要先等到 CSS 文件下载完成并生成 CSSOM ,然后再执行 JavaScript 脚本,最后再继续构建 DOM 。由于这种阻塞,导致了 解析白屏

优化方案:

  • 移除 jscss 的文件下载 :通过内联 JavaScript 、内联 CSS
  • 尽量减少文件大小 :如通过 webpack 等工具 移除 不必要的 注释 ,并 压缩 js 文件
  • 将不进行 DOM 操作或 CSS 样式修改的 JavaScript 标记上 sync 或者 defer 异步引入;
  • 使用媒体查询属性 :将大的 CSS 文件拆分成多个不同用途的 CSS 文件,只有在特定的场景下才会加载特定的 CSS 文件。

可以通过浏览器调试工具的 Network 面板中的 DOMContentLoaded 查看最后生成 DOM 树所需的时间;

7.样式计算

  • 15、 转换样式表中的属性值,使其标准化。比如将 em 转换为 pxcolor 转换为 rgb
  • 16、 计算 DOM 树中每个节点的具体样式,这里遵循 CSS 的继承和层叠规则;可以通过 Chrome 调试工具的 Elements 选项的 Computed 查看某一标签的最终样式;

8.布局阶段

  • 17、创建布局树,遍历 DOM 树中的所有节点,去掉所有隐藏的节点(比如 head ,添加了 display:none 的节点),只在布局树中保留可见的节点。

  • 18、计算布局树中节点的坐标位置(较复杂,这里不展开);

9.分层

  • 19、 对布局树进行分层,并生成分层树( Layer Tree ),可以通过 Chrome 调试工具的 Layer 选项查看。分层树中每一个节点都直接或间接的属于一个图层(如果一个节点没有对应的层,那么这个节点就从属于父节点的图层)

10.图层绘制

  • 20、 为每个图层生成绘制列表(即绘制指令),并将其提交到合成线程。以上操作都是在渲染进程中的主线程中进行的,提交到合成线程后就不阻塞主线程了;

渲染进程中的合成线程部分

11.切分图块

21、合成线程将图层切分成大小固定的图块( 256x256 或者 512x512 )然后 优先绘制 靠近视口的图块,这样就可以大大加速页面的显示速度;

Ⅳ. GPU 进程

12.栅格化操作

  • 22、光栅化线程池 中将 图块 转换成 位图 ,通常这个过程都会使用 GPU 来加速生成,使用 GPU 生成位图的过程叫 快速栅格化 ,或者 GPU 栅格化,生成的位图被保存在 GPU 内存中。

Ⅴ.浏览器主进程

13.合成与显示

  • 23、 合成:一旦所有图块都被光栅化, 合成线程 就会将它们合成为一张图片,并生成一个绘制图块的命令——“ DrawQuad ”,然后将该命令提交给浏览器进程。

注意了:合成的过程是在渲染进程的 合成线程 中完成的,不会影响到渲染进程的 主线程 执行;

  • 24、 显示:浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。

到这里,经过这一系列的阶段,编写好的 HTMLCSSJavaScript 等文件,经过浏览器就会显示出漂亮的页面了。

参考资料: 浏览器工作原理与实践