面试官:输入url到浏览器页面展示,CPU转了几圈😏😏

浏览器作为我们经常使用的软件,你是否有认真想过,当我们在浏览器中输入一个 URL 或者直接点开一个 URL 后,就可以看到网页内容了。它为什么能显示出来,你是否有考虑过,你是否清楚地知道它背后都经历了什么过程。

那么从今天的文章中我们就来看看这到底经历了什么过程。

什么是 URL

URL 也叫统一资源定位符,它是互联网上用于标识和定位资源的地址。它是一个字符串,用于描述互联网上的资源位置,俗称网页地址,简称网址。URL 通常用于访问网页、图像、视频、文档和其他类型的文件。

URL 由多个部分组成,包括协议、主机名、端口号、路径和查询字符串等。以下是 URL 的一般结构:

协议://主机名[:端口号]/路径?查询字符串

  • 协议: 指定了访问资源时使用的通信协议,例如 HTTPHTTPS;
  • 主机名: 指定了存储资源的服务器的域名或 IP 地址;
  • 端口号: 它是可选的,用于标识服务器上特定的服务;
  • 路径: 指定了资源在服务器上的位置;
  • 查询字符串: 包含了向服务器发送的额外参数,通常用于搜索、过滤或传递信息;

主机名负责的是主机到主机之间的通信,而端口号是进程到进程之间的通信,首先要主机和主机之间的通信,再有进程到进程之间的通信。

以下是一个示例 URL:

https://www.example.com:8080/path/to/resource?param1=value1¶m2=value2

在这个示例中:

  • 协议是 HTTPS;
  • 主机名是 www.example.com;
  • 端口号是 8080;
  • 路径是 /path/to/resource;
  • 查询字符串是 param1=value1&param2=value2;

通过解析 URL,浏览器或其他客户端可以向服务器发起请求,并获取对应的资源。

URL 编码

网络标准规定 URL 必须使用 ASCII 字符集,它必须仅包含 ASCII 字符,即在 ASCII 字符集范围内的字符。ASCII 字符集包括数字、大写字母、小写字母和一些特殊字符如 -、\_、.、~ 等,除此之外,还包括了一些保留字 : / ? # [ ] @ ! $ & ' ( ) * + , ; =。非 ASCII 字符,如中文、日文、希腊字母等,需要进行 URL 编码。

URL 不能包含空格和特殊字符: URL 中不能包含空格和一些特殊字符,如制表符、换行符、回车符等。这些字符在 URL 中具有特殊含义或被用作分隔符,因此需要进行 URL 编码。

URL 在一般情况下应该使用 UTF-8 编码来确保对多语言字符的支持,它是一种通用的字符编码标准,支持几乎所有语言字符,包括中文、日文、韩文等。采用 UTF-8 编码可以确保 URL 能够正确表示和传输各种语言的字符,促进全球范围内的多语言应用和交流。并且 UTF-8 编码是一种安全的编码方式,能够避免安全漏洞和误解释。通过对 URL 中的特殊字符和非 ASCII 字符进行 UTF-8 编码,可以确保 URL 在传输和解析过程中不会引发歧义、安全问题或错误解析。

然而,有时候可能会选择 GB2312 编码方式来进行编码,主要可能是在一些旧的系统或特定的环境中,可能存在对 GB2312 编码的依赖或限制。这些系统或浏览器可能没有完全支持或默认使用 UTF-8 编码,因此在与这些系统或浏览器交互的场景下,可能需要使用 GB2312 编码。

如何统一编码方式

因为 URL 的编码方式有多种,我们无法确保浏览器使用 UTF-8 编码,我们可以采取以下几种措施来尽量保证 URL 的编码一致性:

  • 声明字符编码: 在 HTML 文档的头部中使用 <meta> 标签明确指定字符编码为 UTF-8,例如:这将告诉浏览器应该使用 UTF-8 来解析页面和处理 URL。
  • 使用 URL 编码工具: 在生成 URL 时,可以使用 javascript 提供的 encodeURIComponent() 函数来进行编码。

DNS 相关知识

我们平常在访问某个网站时不适用 IP 地址,而是用一串由罗马字和点组成的字符串。而一般用户在使用 TCP/IP 进行通信时也不适用 IP 地址,能够这样做是因为有了 DNS 功能的支持。它可以将那串字符串自动壮观为具体的 IP 地址。

域名服务器

域名服务器是指管理域名的主机和相应的软件,它可以管理所在分层的域的相关信息。其所管理的分层叫作 ZONE。如下图所示:

各个域的分层上都有设有各自的域名服务器,各层域名服务器都了解该以下分层中所有域名服务器的的地址。

因此 它们从根域名服务器开始呈现树状结构相互连接。

由于所有域名服务器都了解根域名服务器的 IP 地址,所以若从根开始按顺序追踪,可以访问世界上所有域名服务器的地址。

解析器

进行 DNS 查询的主机和软件叫作 DNS 解析器。用户所使用的工作站或个人电脑都属于解析器。一个解析器至少要注册一个以上域名服务器的 IP 地址。通常,它至少包括组织内部的域名服务器的 IP 地址。

DNS 域名解析

首先当我们在浏览器中输入 www.baidu.com 域名,操作系统会先查 hosts 文件是否有记录,如果有的话会把相对应的 IP 返回。

Windows 操作系统中,hosts 文件的路径为 C:\Windows\System32\drivers\etc\hosts

如果在 hosts 文件中找不到相对应的 IP 地址或域名的映射,就会去查找本地 DNS 解析器上是否有缓存,看是否已经缓存了该一名的 IP 地址,如果找到匹配的缓存项,解析器将返回缓存的 IP 地址。

Windows 操作系统中,打开命令提示符窗口,按下 Win 键+ R,输入 cmd 并按下回车键。在命令提示符窗口中,输入以下命令并按下回车键 ipconfig /displaydns 这将显示计算机上的 DNS 解析器缓存中的所有条目,包括域名、IP 地址和缓存的时间,如下图所示:

如果使用 ipconfig /displaydns 命令查找出的地址无法正常使用,可能有以下几个原因:

  • DNS 缓存已过期: 本地 DNS 解析器的缓存中存储的解析结果有一个生存时间 TTL,一旦超过该时间,缓存中的解析结果将被认为过期。在过期之后,本地 DNS 解析器将重新向上级 DNS 服务器发起查询,以获取最新的 IP 地址。因此,如果 ipconfig /displaydns 显示的地址不可用,可能是因为缓存中的解析结果已过期;
  • DNS 缓存已被清除: 有时可能会手动或通过其他操作清除了本地 DNS 解析器的缓存。如果缓存被清除,之前的解析结果将不再可用,需要重新进行 DNS 查询;

如果你发现 ipconfig /displaydns 显示的地址不可用,可以尝试清除本地 DNS 缓存并重新进行 DNS 查询,以获取最新的解析结果。在 Windows 操作系统上,可以使用以下命令来清除 DNS 缓存: ipconfig /flushdns。执行完该命令后,再次进行 DNS 查询,看看是否能够获得正确的 IP 地址。

如果本地 DNS 缓存中没有找到匹配的 IP 地址,本地 DNS 解析器将执行递归查询的过程。它会向配置的上级 DNS 服务器发起查询请求,询问该域名的 IP 地址:

  • 本地 DNS 解析器会发起递归查询: 本地 DNS 解析器将向上级 DNS 服务器发起递归查询,以获取域名的 IP 地址。它会发送查询请求给配置的上级 DNS 服务器,并等待该服务器的响应;
  • 上级 DNS 服务器进行查询: 上级 DNS 服务器将接收到查询请求,并尝试解析该域名。如果上级 DNS 服务器可以解析该域名,则会返回相应的 IP 地址。如果上级 DNS 服务器也无法解析该域名,它可能会继续向更高级的 DNS 服务器发起查询请求,直到找到能够提供域名解析结果的 DNS 服务器为止;
  • 解析失败: 如果在所有的递归查询步骤中都没有找到对应的 IP 地址,本地 DNS 解析器将返回解析失败的结果给应用程序。这意味着应用程序将无法获取所需域名的 IP 地址,并可能导致连接错误或无法访问该网站;

DNS 查询过程中,又有两种常见的处理方式:

  1. 转发模式: 在转发模式下,本地 DNS 解析器将查询请求转发给配置的上级 DNS 服务器,而不直接执行递归查询或迭代查询。本地 DNS 解析器只负责将查询请求发送给上级服务器,并将上级服务器返回的结果直接返回给客户端。转发模式可以减轻本地 DNS 解析器的工作负载,将解析请求转交给上级 DNS 服务器来处理。这样可以节省解析的时间和带宽,并且可以利用上级 DNS 服务器的缓存和优化功能;
  2. 非转发模式: 上述中讲到的就是非转发模式,在非转发模式下,本地 DNS 解析器会直接执行 递归查询 或者 迭代查询 过程,以获取完整的解析结果。它会根据 DNS 协议规范,依次向根 DNS 服务器、顶级域 DNS 服务器和权威 DNS 服务器发起查询请求,直到获得所需的解析结果。非转发模式下的本地 DNS 解析器负责完成整个 DNS 查询过程,包括递归或者迭代查询的执行和结果的返回给客户端。这种模式下,本地 DNS 解析器可能会维护自己的缓存,以提高查询效率和响应速度;

转发模式和非转发模式是本地 DNS 解析器的配置选项,用于控制 DNS 查询的处理方式。选择哪种模式取决于网络环境和需求。

DNS 预加载

当浏览器从第三方服务器请求资源时,必须先将跨源域名解析为 IP 地址,然后浏览器才能发出请求。此过程称为 DNS 解析。DNS 缓存可以帮助减少此延迟,而 DNS 解析可以导致请求增加明显的延迟。对于打开了与许多第三方的连接的网站,此延迟可能会大大降低加载性能。

dns-prefetch 可帮助开发人员掩盖 DNS 解析延迟。HTML <link> 元素通过 dns-prefetch rel 属性值提供此功能。然后在 href 属性中指明要跨源的域名:

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <link rel="dns-prefetch" href="https://fonts.googleapis.com/" />
    <!-- 其他 head 元素 -->
  </head>
  <body>
    <!-- 你的页面内容 -->
  </body>
</html>

其次,你还可以通过使用 HTTP Link 字符将 dns-prefetch 指定为 http 头部字段:

Link: <https://fonts.googleapis.com/>; rel=dns-prefetch

再其次,考虑将 dns-prefetchpreconnect 提示配对。dns-prefetch 只执行 DNS 查询,而 preconnect 则是建立与服务器的连接。这个过程包括 DNS 解析,以及建立 TCP 连接,如果是 HTTPS 网站,就进一步执行 TLS 握手。将这两者结合起来,可以进一步减少跨源请求的感知延迟。你可以像这样安全地将它们结合起来使用:

<link rel="preconnect" href="https://fonts.googleapis.com/" crossorigin />
<link rel="dns-prefetch" href="https://fonts.googleapis.com/" />

TCP 三次握手

当查找到 IP 之后,接下来进行的就是 TCP 连接,以获取相关资源,如 HTMLCSS

因为 TCP 是一种可靠的传输协议,而三次握手是建立 TCP 连接的过程。它确保了通信双方之间的同步和可靠性。下面是 TCP 三次握手的详细过程和原因:

  1. 第一次握手: 客户端向服务端发送一个带有 SYN 标记的 TCP 数据包。该数据包中包含客户端的初始序列号和其他连接信息。
  2. 第二次握手: 服务器收到客户端请求后,将确认信号 ACKSYN 标记一起发送回客户端。服务器还会为客户端分配自己的初始序列号,并将其发送给客户端。
  3. 第三次握手: 客户端收到服务器的响应后,向服务器发送一个确认信号 ACK。客户端的确认信号中包含服务器分配的初始序列号加 1 的值。服务器收到客户端的确认信号后,进入已建立状态,客户端也进入已建立状态。现在,TCP 连接已经建立,双方可以开始进行数据传输。

TCP 之所以采用三次握手,目的是确保客户端和服务器都能正确地建立起可靠的连接,并同步序列号以进行数据传输。通过三次握手,服务器可以确认客户端的接收能力和可用性,而客户端也可以确认服务器的接收能力和可用性。双方都能确保对方愿意接收和处理数据。

计算机网络加餐

从输入 URL 到浏览器展示页面的过程中,计算机网络涉及到多个层级和协议,下面是一个简单的描述,从 OSI 七层模型开始:

  1. 物理层: 用户在浏览器中输入 URL,用户的计算机将点新高转换为二进制数据以便在计算机网络上传输;
  2. 数据链路层: 用户输入的 URL 经过浏览器处理后,进入操作系统的网络协议栈。操作系统将 URL 封装成数据帧,并添加源和目标 MAC 地址。数据帧通过网络接口卡 NIC 传输到本地网络;
  3. 网络层: 操作系统根据 URL 中的主机名解析出目标 IP 地址。如果主机名还没有解析为 IP 地址,操作系统将查询域名系统 DNS 服务器以获取目标 IP 地址。操作系统将数据帧封装成数据包,并添加源和目标 IP 地址。通过路由选择算法,确定数据包的下一跳路由器;
  4. 传输层: 操作系统为 URL 请求建立传输层连接,通常使用传输控制协议 TCP 以保证可靠传。操作系统将 URL 请求封装成 TCP 报文段,并添加源和目标端口号;
  5. 会话层: 操作系统与目标服务器建立会话,协商会话参数,可能使用传输层安全协议 TLS/SSL 进行加密;
  6. 表示层: 在 Web 浏览器的应用层,将 URL 请求转换为 HTTP 请求报文;
  7. 应用层: 操作系统将 HTTP 请求报文发送到目标服务器的应用程序。服务器接收到请求后,根据 URL 请求的资源类型,如 HTMLCSSJavaScript 等进行处理。服务器生成 HTTP 响应报文,并将响应报文返回操作系统的网络协议栈;
  8. 表示层和会话层: 操作系统对服务器的 HTTP 响应报文进行解码和解密;
  9. 传输层: 操作系统将 HTTP 报文响应报文封装成 TCP 报文段,并添加源和目标端口号。操作系统将通过网络传输层将 TCP 报文段发送回用户的计算机;
  10. 网络层: 数据包经过网络层路由器的转发,通过多个网络节点传输,最终到达用户的计算机;
  11. 数据链路层: 数据包到达用户计算机后,操作系统将其解析为数据帧,去除源和目标 MAC 地址;
  12. 物理层: 用户计算机将二进制数据转换为电信号,以便在用户界面上展示页面;
  13. 应用层: 操作系统将解析后的 HTTP 响应报文传递给浏览器。浏览器对 HTMLCSSJavaScript 等资源进行解析和渲染,构建文档对象模型 DOM。浏览器将渲染后的页面内容显示在用户界面上,用户可以看到浏览器展示的页面;

需要注意的是,这只是一个大致的流程描述,实际过程中可能会涉及更多细节和特殊情况。计算机网络是一个复杂的系统,涉及多种协议和技术,用于实现从 URL 输入到页面展示的整个过程。

在后续的文章中可能会详细写一篇关于网络篇的详细流程,这里就不再展开描述了。

强制缓存和协商缓存

TCP 三次握手之后,客户端和服务器建立连接,就该请求 html 等文件了,如果这些文件在缓存里面浏览器直接返回,如果没有,就去后台拿。这也是为什么第二次打开网页的时候会把第一次打开速度会快很多的原因了

这个就涉及到了强制缓存和协商缓存了。后续内容会比较多,这个知识点的内容比较多,更详细的内容可以看我之前写的这篇文章,里面很详细地讲到了强制缓存和协商缓存。

强制缓存这么暴力,为什么不使用协商缓存 😡😡😡

浏览器渲染机制

谷歌浏览器是一款多进程的浏览器,它采用了多进程架构来实现更高的稳定性和安全性。具体来说,谷歌浏览器在打开一个页面时会启动多个进程,主要包括以下几个进程:

  • 主进程: 它是浏览器的核心进,负责管理和协调其他进程的工作。它处理用户界面、用户输入以及各个渲染进程的管理和控制;
  • 渲染进程: 每个标签页都会有一个独立的渲染进程,它负责加载和渲染网页内容,将 HTMLCSSJavaScript 转换成可视化页面;
  • GPU 进程: 它负责处理与图形相关的任务,如页面的绘制和 3D 渲染等。通过将图形任务交给独立的 GPU 进程,能够实现更好的性能和资源利用率;
  • 网络进程: 谷歌浏览器有一个独立的网络进程,负责处理所有与网络相关的任务。它管理所有的网络请求和资源加载,并在渲染进程之间共享缓存数据;

除了上述主要进程外,谷歌浏览器还可能存在一些其他辅助进程,如插件进程、扩展进程等,它们用于运行第三方插件和扩展程序。

网络进程

网络进程复制发起和处理浏览器中的所有网络请求。挡用户在浏览器中输入 RUL、点击链接或提交表单时,网络进程会接收这些请求,并根据请求类型,例如 HTTPHTTPSWebSocket 等进行处理。

首先,当浏览器进程接收到用户输入的 URL 请求,浏览器进程便将该 URL 转发给网络进程,然后在网络进程中发起真正的 URL 请求。接着网络进程接收到了响应头数据,便解析响应头数据,并将数据转发给浏览器进程。

浏览器进程接收到网络进程的响应头数据之后,发送 提交导航 消息到渲染进程。

渲染进程

当浏览器进程接收到网络进程的响应头数据之后,它会根据当前的状态进行判断:

  • 如果有空闲的渲染进程,浏览器进程选择一个空闲的渲染进程,并将 HTML 数据提交给该渲染进程;
  • 如果没有空闲的渲染进程,浏览器进程会创建一个新的渲染进程,并将 HTML 数据提交给新创建的渲染进程;

浏览器进程将 HTML 数据通过 IPC 机制发送给选定的渲染进程,渲染进程接收到 HTML 数据后,开始解析和渲染页面。

进程之间通信

在前面的网络进程、浏览器进程和渲染进程之间的通信都是使用 IPC 机制实现进程间的通信。

在谷歌浏览器中,常见的进程间通信方式包括以下几种:

  1. 基于管道的通信:浏览器进程和渲染进程之间可以通过管道进行通信;浏览器进程和渲染进程在操作系统级别建立管道连接,可以在管道上进行数据传输;
  2. 基于共享内存的通信:
  • 浏览器进程和渲染进程之间可以使用共享内存进行通信;
  • 共享内存是一块可以被多个进程访问的内存空间,浏览器进程和渲染进程可以通过共享内存传递数据;
  1. 消息传递:浏览器进程和渲染进程之间可以使用消息传递机制进行通信;浏览器进程和渲染进程通过发送和接收消息来交换数据;

管道通信是一种进程通信 IPC 的方式,用于在浏览器进程和渲染进程之间进行数据传输。在谷歌浏览器中,浏览器进程和渲染进程之间可以通过管道进行通信。以下是管道通信的一般流程:

  1. 创建管道: 在操作系统级别,浏览器进程和渲染进程创建一个管道,它是一个单向的通信信道,可以在两个进程之间传输数据;
  2. 数据传输: 浏览器进程将需要传输的数据写入管道的写段。渲染进程从管道的读端读取数据,数据通过管道在浏览器进程和渲染进程之间进行传输;
  3. 数据处理: 渲染进程接收到从管道进程读取的数据后,进行响应的处理,数据可能包括 HTMLCSSJavaScript、渲染命令等;
  4. 返回结果: 渲染进程处理完数据后,将处理结果,如渲染后的页面内容等写入到管道的写段。浏览器进程从管道的读端读取处理结果;

通过管道通信,浏览器进程和渲染进程可以在两个进程之间传递数据,实现页面内容的交换和协作。管道通信是一种高效的 IPC 机制,可以快速传输大量数据,并且在操作系统级别提供了一定的隔离性和安全性。然而,管道通信是一种单向通信方式,需要使用双向管道或双管道来实现双向通信。

构建 DOM 树

由于浏览器无法直接理解和使用 HTML,所以需要由 HTML 解析器将 HTML 转换为浏览器能够理解的结构 DOM 树。

整个 HTMLCSS 的解析流程到效果展示如下图所示,我们这个部分主要来讲讲 HTML 部分。

The tokenization algorithm

The tokenization algorithm 翻译成中文意思为标记化算法,也就是词法分析。来吧,接下来我们看一段代码示例吧,看看它是怎么被标记化成一系列的 tokens 的,如下代码所示:

<html>
  <body>
    <p>Hello World</p>
    <div><img src="example.png" /></div>
  </body>
</html>

  1. 起始标记: <html> 起始标记表示根元素 <html> 的开始;
  2. 起始标记: <body> 起始标记表示 <body> 元素的开始;
  3. 起始标记: <p> 起始标记表示 <p> 元素的开始;
  4. 文本内容: Hello World 表示 <p> 元素内的文本内容;
  5. 结束标记: </p> 结束标记表示 <p> 元素的结束;
  6. 起始标记: <div> 起始标记表示 <div> 元素的开始;
  7. 起始标记: <img> 起始标记表示 <img> 元素的开始;
  8. 属性: src="example.png"<img> 元素的 src 属性设置为 example.png;
  9. 自闭合标记: /> 表示 <img> 元素的自闭合;
  10. 结束标记: </div> 结束标记表示 <div> 元素的结束;
  11. 结束标记: </body> 结束标记表示 <body> 元素的结束;
  12. 结束标记: </html> 结束标记表示根元素 <html> 的结束;

经过标记化处理后,该 HTML 代码会被解析为一系列的标记。这些标记将被后续的操作用于构建 DOM 树的节点结构。

当创建完成第一个 token 之后,树构建开始。这实际上是基于先前解析的标签创建树状结构,称之为文档对象模型。

DOM 树描述了 HTML 文档的内容,<html> 元素是文档树的第一个标签和根节点,树反映了不同标签之间的关系和层次结构。下图是上面的代码示例构建出来的 DOM 树:

值得注意的是,此构建阶段是可重入的,这意味着在处理一个 token 时,可能会恢复解析器,这导致在第一个 token 处理完成之前触发并处理更多 token

这是因为浏览器在构建 DOm 树时采用了逐步增量的方式,通过逐个解析和处理标记来逐步构建 DOM 树的节点结构。它的意义在于浏览器在构建 DOM 树的过程中可以终端,然后再次从中断点继续执行。这是非常重要的,因为在处理大型 HTML 文档或者遇到复杂的标记结构时,构建 DOm 树可能需要一定的时间。如果这个过程不可重入,即不可能在中断处继续执行,那么解析大型文档或复杂结构的效率会受到影响。

可重入性使得浏览器可以根据需要进行渐进式的渲染和交互,而不必等待整个 DOM 树完全构建完成。这种能力使得浏览器能够更快地开始显示页面内容,并允许用户在页面加载过程中进行交互。

样式计算

样式计算的目的是为了计算出 DOM 节点中每个元素的具体样式,,这个阶段大体可分为三步来完成。

CSS 解析

CSS 样式的来源主要有哪些呢,它的样式来源主要有以下三种:

  • 通过 link;
  • 引用的外部;
  • CSS 文件;

HTML 文件一样,浏览器也是无法直接理解这些纯文本的 CSS 样式,所以当渲染进程接收到 CSS 文本时,会执行一个转换操作,将 CSS 文本转换为浏览器可以理解的结构,也就是 styleSheets

Style Sheets

styleSheets 是浏览器提供的一个接口,用于访问和操作页面中的样式表。通过 styleSheets 对象,可以动态地获取和修改样式表的规则、属性和值,从而实现对页面样式的控制和操作。

你可以在 Chrome 控制台中查看其结构,只需要在控制台中输入 document.styleSheets,可以使用该属性来获取页面中所有的样式表,如下图所示:

转换样式表中的属性值,使其标准化

这个时候我们已经把现有的 CSS 文本转化为浏览器可以理解的结构了,那么接下来就要对其进行属性值的标准化操作。

在谷歌浏览器中,样式表中的属性值转换是由浏览器的渲染引擎处理的。当解析和应用样式表时,渲染引擎会根据属性值的具体类型和单位进行转换,以正确计算和渲染页面元素的样式。

下面是一些常见的属性值转换示例:

  1. 长度单位转换: 对于指定长度的属性值,如 widthheightmarginpadding 等,渲染引擎会根据具体情况将其转换为适当的像素值。例如,可以使用 pxemrem%等单位,在渲染阶段会将其转换为适应屏幕尺寸和布局的像素值;
  2. 颜色值转换: 对于颜色属性值,如 colorbackground-color 等,渲染引擎会将其转换为浏览器能够理解和显示的颜色格式,如 RGBRGBA、十六进制等;
  3. 图像路径转换: 对于样式中的图像路径,如 background-imagelist-style-image 等,渲染引擎会根据指定的路径解析和加载图像资源,并将其正确应用于对应的元素;
  4. 百分比转换: 对于一些属性值,如 widthheight 等,使用百分比单位时,渲染引擎会根据父元素或容器的尺寸计算百分比的具体像素值,以适应不同的屏幕大小和布局;

在实际的项目中,我们有时候会编写 emrem这些 css 代码,实际上会被编译成具体的 px 值。

计算出 DOM 树中每个节点的具体样式

现在的样式的属性已经被标准化, 接下来就需要计算 DOM 树中每个节点的样式属性了,这就到了图中的这一步了:

Chrome 中,它会遍历 DOM 树中的每个节点,根据节点的标签名、类名、ID等属性,匹配对应的样式规则。对于匹配到的样式规则,谷歌浏览器会计算其具体的样式值。这包括继承样式的计算、属性值的层叠计算以及特定选择器的样式应用。根据 DOM 树和计算得到的样式信息,构建渲染树Render Tree,也就是后面那一步。渲染树包含了页面中需要渲染的节点和其对应的样式信息。

这样说可能会有点抽象,我们看下面的一个例子,如下代码所示:

body {
  font-size: 20px;
}
p {
  color: blue;
}
span {
  display: none;
}
div {
  font-weight: bold;
  color: red;
}
div p {
  color: green;
}

这张样式表最终应用到 DOM 节点的效果如下图所示:

在样式计算过程中,谷歌浏览器会考虑以下因素:

  • 继承: 某些样式属性是可以继承的,例如字体、颜色等。当节点自身没有指定某个样式属性时,会继承其父节点的对应样式属性;
  • 层叠: 当同一个节点上存在多个样式规则时,根据层叠顺序和选择器的权重来决定最终应用哪个样式规则。层叠顺序由样式规则的出现顺序和特殊性 Specificity 决定; Specificity: 在 CSS 中,Specificity 是一种用于确定样式规则优先级的机制。它决定了在多个样式规则应用到同一个元素时,哪个规则将具有最终的样式效果。 在 HTML 中,元素的 <style> 属性的值是样式表规则。这些规则没有选择器,所以 a=1, b=0, c=0, d=0;计算选择器中 ID 属性的个数,也就是 a=0, b=1, c=0, d=0;计算选择器中其他属性和伪类的数量,也就是 a=0, b=0, c=1, d=0;计算选择器中元素名称和伪元素的数量,也就是 a=0, b=0, c=0, d=1; 请看下面的例子,如下所示:

当这个完成之后,就会生成一棵 渲染树

布局

渲染树包含有关显示哪些节点及其计算样式的信息,但不包含每个节点的尺寸或位置。那么接下来就需要计算出 DOM 树中可见元素的几何位置,我们把这个过程叫做布局。

创建布局树

根据 DOM 树和计算得到的样式信息,浏览器会创建布局树。布局树包含了需要参与页面布局的元素,如可见的文本、块级元素、行内元素等,而一些不参与布局的元素,如脚本、注释等则不包含在布局树中。

在创建布局树时,浏览器会对每个元素进行布局计算。布局计算过程会根据元素的样式属性,计算元素的位置、大小、边距等布局信息。

分层

现在我们有了布局树,而且每个元素的具体位置信息都计算出来了,但是接下来并不是就要开始着手绘制页面了。

因为页面中有很多复杂的效果,如一些复杂的 3D 变换、页面滚动,或者使用 z-indexingz 轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树。

在浏览器中,分层 Layering 是一种优化技术,用于提高页面的渲染性能和效果。分层将页面内容划分为多个独立的图层,每个图层可以独立地进行绘制、合成和重绘,从而实现更高效的页面渲染和交互。

下面是谷歌浏览器中实现分层的大致过程:

  1. 创建图层: 根据布局树的结构和样式信息,浏览器会将页面内容划分为不同的图层。通常,具有一定复杂度的元素,如动画、视频、3D 变换等会被独立划分为一个图层,以便进行独立的绘制和合成;
  2. 图层绘制: 每个图层都有自己的绘制表面,浏览器会将图层中的元素绘制到对应的绘制表面上。这样,当图层中的元素发生变化时,只需要重新绘制对应的图层,而不需要重新绘制整个页面;
  3. 图层合成: 合成是指将各个图层的绘制结果合并成最终的页面显示。合成过程包括将各个图层的绘制表面进行合成、进行透明度混合、应用滤镜效果等。合成操作可以在 GPU 硬件加速下进行,提高渲染性能;
  4. 分层优化: 为了提高性能,浏览器会对图层进行一些优化处理。例如,浏览器可以根据图层的可见性和变化频率来决定是否进行绘制,以避免不必要的绘制操作。浏览器还可以根据硬件能力和页面复杂度等因素,动态地调整图层的划分策略;

通过分层,浏览器可以实现更高效的页面渲染和交互效果。分层技术可以提高页面的绘制速度、减少页面的重绘区域、降低页面的内存占用,并能够利用硬件加速和优化来提供更流畅的动画和交互效果。

要想直观地理解什么是图层,你可以打开 Chrome 的开发者工具,选择 Layers 标签就可以可视化页面的分层情况,如下图所示:

至于什么样的标签或者设置什么的样式会被分层,不是这篇文章要讲的,感兴趣的可以自行去学习,说实话我,我 css 老菜了,啥也不会。

图层绘制

在完成图层树的构建之后,渲染引擎会对图层树中的每个图层进行绘制,那么接下来我们看看渲染引擎是怎么实现图层绘制的?

浏览器会将每个需要绘制的元素及其样式信息转化为绘制记录,记录了元素的绘制命令和相关的绘制参数。这些绘制记录会被添加到绘制列表中。

根据绘制记录的绘制顺序和层叠关系,浏览器会对绘制列表进行排序,以确保正确的绘制顺序。通常,位于上层的元素会覆盖位于下层的元素,一个绘制列表如下图所示:

所以在图层绘制阶段,输出的内容就是这些待绘制列表。

栅格化操作

绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来完成的。

在谷歌浏览器中,渲染主线程和合成线程是两个关键线程,它们共同协作来完成页面的渲染和显示:

  • 绘制命令传递: 渲染主线程会生成绘制命令,将绘制内容传递给合成线程。这包括绘制记录、栅格化结果、纹理数据等;
  • 图层合成: 合成线程接收到来自渲染主线程的绘制内容后,根据图层的层叠关系和样式属性,进行图层的合成操作。合成线程将多个图层的绘制结果进行混合和合并,生成最终的页面图像;
  • 纹理上传: 合成线程会将合成结果中的纹理数据上传到 GPU,以便进行硬件加速的渲染和显示;
  • 显示刷新: 合成线程将合成后的图像传递给显示设备进行显示。它与显示设备进行交互,按照显示设备的刷新率和时间间隔,定期更新屏幕上的图像内容;

渲染主线程和合成线程之间的关系如下图所示:

通常情况下,合成线程根据图层的尺寸和显示区域,将图层划分为多个图块。图块的大小可以根据具体情况进行调整,通常为 256x256 像素或 512x512 像素。

然后合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。而图块是栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的,运行方式如下图所示:

通过使用栅格化线程池,渲染进程能够充分利用多核处理器的并行性能,将栅格化任务并行执行,提高栅格化的效率和速度。这对于处理复杂的页面、大量的矢量图形和动画效果非常有益,可以减少页面的渲染延迟和提升用户体验。

光速栅格化是一种优化的栅格化算法,旨在加速栅格化的速度。它通过减少不必要的计算和优化算法细节,以更高效地执行栅格化操作。通常包括以下技术:

  • 并行计算: 使用并行计算技术,如 SIMD 指令或 GPU 计算,对多个像素进行并行处理,加速栅格化的速度;
  • 剔除和裁剪: 通过剔除或裁剪不可见的像素片段,避免对它们进行不必要的计算和绘制。这可以提高栅格化的效率;
  • 低分辨率栅格化: 对于屏幕上较小的对象或远处的对象,可以使用低分辨率的栅格化来减少计算和绘制的工作量。这种方法可以在保持视觉质量的前提下提高栅格化的速度;

合成和显示

一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令 DrawQuad

在谷歌浏览器的渲染流程中,Draw QuadCompositor Frame 是两个重要的概念,它们用于描述渲染过程中的绘制指令和渲染结果。下面详细解释它们的含义:

  • Draw Quad: Draw Quad 是一种描述绘制操作的指令,用于告诉渲染引擎如何在屏幕上绘制图形对象。它包含了一些基本信息,如位置、大小、颜色、纹理坐标等,用于定义绘制对象的属性。它通常用于描述页面上的元素,如 HTML 元素、图像、视频等。每个元素都可以由一个或多个 Draw Quad 组成,根据需要进行绘制;
  • Compositor Frame: Compositor Frame 是在渲染进程中的一个阶段,表示经过合成线程处理后的最终渲染结果。它包含了所有需要在屏幕上显示的图块的合成图像。由多个图块(Tiles)组成,每个图块代表了页面的一个部分或图层。这些图块包含了栅格化后的位图信息,经过合成操作后,形成了整个页面的最终渲染图像;

在渲染过程中,浏览器通过将页面内容划分为多个图块,每个图块对应一个 Draw Quad 或一组 Draw Quad。这些 Draw Quad 描述了各个元素的位置、大小和属性等信息。合成线程将这些 Draw Quad 根据其在页面中的层级关系、透明度和其他效果进行合成,生成 Compositor Frame,即最终的渲染结果。

Compositor Frame 表示了一帧的渲染结果,生成了 Compositor Frame 以后,Viz 会调用 GL 指令把 Draw Quad 最终输出到屏幕上。

在谷歌浏览器中,VIZ 组件与 OpenGL(GL) 密切相关。VIZ 组件负责将合成线程生成的 Compositor Frame 转换为 OpenGL 绘图指令,并利用 GPU 的硬件加速功能执行这些指令,以实现高效的图形渲染和页面显示。VIZ 组件与 GL 的结合使得谷歌浏览器能够提供流畅的页面滚动、动画和交互效果。

ending......

总结

整篇文章加起来 2w 多字,其实要想深挖,还有很多内容可以进行详细讲解的。

可以说的是,这篇文章是有有史以来写得最长最认真的一篇。回想过之前写的文章,好像很多文章都是和这篇文章有关,而写的这些文章也都是为这篇文章做准备。

希望这篇文章对您有帮助,如有不懂,欢迎评论区留下你的疑惑。

参考文章

How browsers work李兵--极客时间《浏览器工作实践与原理》dns-prefetch阿里面试官的”说一下从 url 输入到返回请求的过程“问的难度就是不一样!

相关文章推荐

强制缓存这么暴力,为什么不使用协商缓存 😡😡😡🤓 面试官: 为什么 TCP 连接这么喜欢握手,它们的关系很亲密吗?深入理解 V8 执行流程、执行上下文和作用域!

最后再来回答一下标题这个问题: 在输入 URL 到浏览器页面展示的整个过程中,CPU 会进行多次计算和处理,但无法精确地确定 CPU 转了多少圈。这是因为 CPU 的计算速度非常快,每秒可以执行数亿次的指令。在整个过程中,CPU 会快速切换不同的任务,并进行并行处理和优化,以提高页面加载的速度和性能。CPU 转了多少圈并不是一个明确的概念,因为其计算速度极快,无法以圈数的形式来衡量。CPU 在每个步骤中会进行多次指令的执行和计算,以完成输入 URL 到页面展示的整个过程。

全部评论
所以转了几圈
点赞
送花
回复
分享
发布于 2023-08-02 13:52 北京

相关推荐

8 31 评论
分享
牛客网
牛客企业服务