从输入 URL 到浏览器展示,到底经历了什么 (超级详细!)

面试导航 是一个专注于前、后端技术学习和面试准备的 免费 学习平台,提供系统化的技术栈学习,深入讲解每个知识点的核心原理,帮助开发者构建全面的技术体系。平台还收录了大量真实的校招与社招面经,帮助你快速掌握面试技巧,提升求职竞争力。如果你想加入我们的交流群,欢迎通过微信联系:yunmz777

当我们在浏览器中输入一个 URL 或点击一个链接后,网页内容为何能够顺利呈现?你是否深入思考过这个过程,了解它背后的工作机制?

什么是 URL

URL(统一资源定位符)是互联网上用于标识和定位资源的地址,俗称 网页地址网址。它是一串字符串,描述了资源在互联网上的位置,通常用于访问网页、图像、视频、文档等各种类型的文件。

一个 URL 由多个部分组成,包括 协议主机名端口号 (可选)、路径查询字符串。其一般结构如下:

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

主要部分解析:

  • 协议 (Scheme):指定访问资源时使用的通信协议,如 HTTPHTTPS

  • 主机名 (Host):指向存储资源的服务器,通常是域名或 IP 地址。

  • 端口号 (Port,可选):用于标识服务器上的特定服务,默认端口号可省略,如 HTTP 默认端口为 80HTTPS443

  • 路径(Path):指定资源在服务器上的位置,类似于文件系统的目录结构。

  • 查询字符串 (Query Parameters):用于向服务器传递额外参数,通常用于搜索、过滤或数据传递,格式为 键=值 形式,多个参数用 & 连接。

主机名 负责主机到主机之间的通信,而 端口号 负责进程到进程的通信。首先主机之间要建立连接,然后再通过端口号与具体的进程交互。

以下是一个示例 URL:

text 复制代码
https://www.example.com:8080/path/to/resource?param1=value1&param2=value2

在这个示例中:

  • 协议HTTPS

  • 主机名www.example.com

  • 端口号8080(非默认端口,需显式指定)

  • 路径/path/to/resource

  • 查询字符串param1=value1&param2=value2

浏览器或其他客户端解析 URL,然后向服务器发起请求,最终获取对应的资源。这是网页访问、文件下载、API 调用等网络通信的基础。

URL 编码与字符集

URL 只能使用 ASCII 字符

根据网络标准,URL 必须使用 ASCII 字符集 ,即只能包含 ASCII 范围内的字符,包括:

  • 字母(A-Z, a-z)
  • 数字(0-9)
  • 部分特殊字符- _ . ~
  • 保留字符 (用于 URL 结构):: / ? # [ ] @ ! $ & ' ( ) * + , ; =

URL 不能包含空格和非法字符

URL 不能直接包含空格 、制表符、换行符、回车符等特殊字符,因为它们在 URL 解析中可能导致错误或歧义。因此,这些字符需要进行 URL 编码Percent Encoding),即转换为 % 加上对应的 ASCII 码十六进制表示。例如:

  • 空格 )➡ %20
  • 汉字 "中"UTF-8 编码)➡ %E4%B8%AD

URL 编码应使用 UTF-8

一般情况下,URL 应采用 UTF-8 编码,以支持全球多语言字符。

  • UTF-8 是一种通用字符编码标准,能够正确表示 中文、日文、韩文、希腊文等各种语言
  • UTF-8 编码有助于避免 URL 解析错误,确保数据传输的准确性。
  • UTF-8 还能有效降低安全风险,避免 URL 解析漏洞和误解读。

何时使用 GB2312

虽然 UTF-8URL 编码的主流选择,但在 某些旧系统或特定环境 下,可能会使用 GB2312 进行编码,例如:

  • 老旧的系统 :部分旧版 Web 服务器、浏览器或应用程序可能默认采用 GB2312 编码,未全面支持 UTF-8
  • 历史遗留问题 :某些依赖 GB2312 的网站或接口,为了兼容性可能仍然沿用 GB2312

但从 现代 Web 开发角度 ,推荐统一采用 UTF-8,以保证 URL 的兼容性、可读性和全球化适应性。

如何统一编码方式

由于 URL 可能使用不同的编码方式,我们无法确保浏览器始终采用 UTF-8 进行解析。为了尽可能保证 URL 编码的一致性,可以采取以下措施:

1. 声明字符编码

HTML 文档的 <head> 部分明确指定 UTF-8 编码,确保浏览器正确解析页面和处理 URL

html 复制代码
<meta charset="UTF-8" />

这样,浏览器会按照 UTF-8 解析页面内容,并在 URL 处理中优先使用 UTF-8 编码。

2. 使用 URL 编码工具

在生成 URL 时,可以使用 JavaScript 内置的 encodeURIComponent() 方法,对 URL 进行编码,确保特殊字符和非 ASCII 字符正确转换,避免乱码或解析错误。

js 复制代码
const url = "https://www.example.com/search?query=你好";
const encodedURL = encodeURIComponent(url);
console.log(encodedURL);
// 输出: https%3A%2F%2Fwww.example.com%2Fsearch%3Fquery%3D%E4%BD%A0%E5%A5%BD

这可以确保 URL 传输时不会因字符编码不同而出现问题,从而提高兼容性和稳定性。如下输出所示:

DNS 预加载(DNS Prefetching)

在浏览器向 第三方服务器 请求资源时,必须先将 跨源域名解析为 IP 地址 ,然后才能正式发起请求。这个过程被称为 DNS 解析(DNS Resolution)

尽管 DNS 缓存 可以帮助减少解析时间,但对于涉及多个第三方域名的站点,DNS 解析仍可能增加 明显的延迟,进而影响页面加载性能。

dns-prefetch:减少 DNS 解析延迟

dns-prefetch 允许浏览器 提前解析指定域名的 DNS 记录 ,以减少后续请求的延迟。开发者可以通过 <link> 元素的 rel="dns-prefetch" 来实现:

html 复制代码
<html lang="zh">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />

    <!-- 预解析 Google Fonts 以加快字体加载 -->
    <link rel="dns-prefetch" href="https://fonts.googleapis.com/" />

    <!-- 其他 head 元素 -->
  </head>
  <body>
    <!-- 页面内容 -->
  </body>
</html>

在 HTTP 头中使用 dns-prefetch

除了 HTML <link> 方式,你还可以在 HTTP 响应头 中指定 dns-prefetch,从而让浏览器在接收到 HTTP 头部信息时,就开始进行 DNS 预解析

http 复制代码
Link: <https://fonts.googleapis.com/>; rel=dns-prefetch

结合 dns-prefetchpreconnect

虽然 dns-prefetch 仅进行 DNS 解析 ,但如果希望进一步优化跨源资源的加载速度,可以 结合 preconnect 使用

  • dns-prefetch:只解析 DNS,不建立连接。
  • preconnect提前建立 TCP 连接 ,如果是 HTTPS,还会进行 TLS 握手,进而大幅减少初始请求的延迟。

可以安全地将两者结合使用,示例如下:

html 复制代码
<!-- 提前建立连接,减少延迟 -->
<link rel="preconnect" href="https://fonts.googleapis.com/" crossorigin />

<!-- 预解析 DNS,确保解析速度 -->
<link rel="dns-prefetch" href="https://fonts.googleapis.com/" />

小结

dns-prefetch 适用于 第三方资源 ,如 CDN、字体、广告、分析工具 等。

preconnect 可进一步优化连接过程,适用于 关键资源 ,如 API 服务器、主要资源域名

两者配合使用 ,可以 最大程度降低跨源请求的加载延迟,提升网站性能。

TCP 三次握手:建立可靠的连接

IP 地址解析完成后,浏览器需要 建立 TCP 连接 以获取网页资源(如 HTMLCSS、JavaScript 等)。

TCPTransmission Control Protocol )是一种可靠的传输协议 ,为了确保数据能够稳定、无误地传输TCP 采用 三次握手(Three-Way Handshake) 机制来建立连接。

TCP 三次握手的详细过程

  1. 第一次握手(Client → Server)

    • 客户端向服务器发送一个 SYN(同步)数据包,表示请求建立连接。

    • 该数据包包含 客户端的初始序列号(ISN,Initial Sequence Number) 和其他连接信息。

  2. 第二次握手(Server → Client)

    • 服务器收到 SYN 请求后,返回一个 带有 SYNACK(确认)标志的数据包

    • 服务器分配一个 自己的初始序列号 ,并将其返回给客户端,同时确认客户端的 SYN(即 ACK = 客户端序列号 + 1)。

  3. 第三次握手(Client → Server)

    • 客户端收到服务器的响应后,再次发送一个 ACK 确认包 ,表示已收到服务器的 SYN

    • ACK 的值为 服务器的序列号 + 1 ,确认服务器的 SYN 数据包已成功到达。

    • 服务器收到 ACK 后,TCP 连接正式建立,双方可以开始数据传输。

为什么 TCP 需要三次握手?

确保双方通信能力

  • 服务器必须确认客户端能够发送和接收数据 ,客户端也必须确认服务器能够发送和接收数据

  • 通过 三次握手 ,双方能够同步序列号,确保数据能按正确的顺序传输。

防止历史连接误影响当前连接

  • 如果使用 两次握手 ,可能导致服务器错误地接受过期的连接请求,从而浪费资源或导致数据错误。

  • 三次握手能够有效避免因旧的重复连接请求导致的错误。

保证可靠传输

  • TCP 采用 三次握手 进行连接确认,确保数据能够完整、无误地传输,防止数据丢失或乱序

小结

  • TCP 通过 三次握手 建立可靠的双向连接 ,确保双方都能正常发送和接收数据

  • 该机制能够 同步双方的序列号 ,并确保数据按序到达 ,避免历史连接干扰

  • 这是 TCP 可靠性的重要保障,也是现代网络通信的核心机制之一

强制缓存与协商缓存:优化资源加载

TCP 三次握手完成后,浏览器开始请求 HTMLCSSJavaScript、图片等资源。为了优化性能,浏览器会优先检查 本地缓存 ,决定是否需要向服务器请求资源。这一过程涉及 强制缓存(强缓存)协商缓存(对比缓存),其主要流程如下:

  1. 检查强制缓存(Expires / Cache-Control):

    • 浏览器首先检查本地缓存是否命中

      • Cache-Control: max-age=3600(资源在 3600s 内有效)

      • Expires: Wed, 15 Mar 2025 12:00:00 GMT(资源过期时间)

    • 缓存有效:直接从本地缓存读取,减少网络请求,提升加载速度 🚀。

    • 缓存过期 :进入下一步 协商缓存 机制。

  2. 检查协商缓存(ETag / Last-Modified):

    • 如果强制缓存失效,浏览器向服务器发送请求,包含:

      • If-None-Match: "etag-value"(对比 ETag

      • If-Modified-Since: Wed, 14 Mar 2025 12:00:00 GMT(对比 Last-Modified

    • 服务器检查资源是否修改

      • 未修改(304 Not Modified) :返回 304,浏览器继续使用本地缓存。

      • 已修改(200 OK) :返回 200,服务器发送新的资源,浏览器更新缓存。

  3. 资源下载、解析、渲染:

  • 浏览器解析 HTML,发现 CSS、JS、图片 等子资源

  • 对每个子资源再次检查缓存机制(强制缓存 → 协商缓存)

  • 最终构建 DOMCSSOM,渲染页面

总的来说:

📌 强制缓存Cache-Control / Expires,本地缓存有效时不向服务器请求资源。

📌 协商缓存ETag / Last-Modified,对比资源是否更新,决定是否重新下载。

📌 缓存机制极大优化网页加载速度,减少不必要的请求,提高用户体验! 🚀

谷歌浏览器的多进程架构

谷歌浏览器采用 多进程架构 ,以提升 稳定性、安全性和性能。打开网页时,会启动多个独立进程,主要包括:

  • 主进程:负责管理浏览器界面、用户输入、标签页、进程调度等。

  • 渲染进程 :每个标签页独立运行,负责解析 HTMLCSS、执行 JavaScript 并渲染页面。

  • GPU 进程 :专门处理页面绘制、3D 渲染等任务,提高渲染效率。

  • 网络进程:独立管理网络请求和资源加载,优化多个页面的数据传输。

  • 其他辅助进程:包括插件进程、扩展进程等,用于运行第三方插件和扩展。

多进程架构确保 崩溃隔离沙盒安全机制高效资源管理,提升浏览器的整体体验。 🚀

主进程

主进程(Browser Process)是浏览器的核心控制中心,负责协调各个组件并管理非渲染任务。

它的主要职责有以下几个方面:

  1. 用户界面管理

    • 渲染浏览器框架、地址栏、导航按钮等界面元素

    • 处理窗口和标签页的创建与管理

  2. 导航控制

    • 处理 URL 输入和链接点击

    • 启动渲染进程加载网页

    • 管理浏览历史和前进/后退功能

  3. 进程管理

    • 创建和监控渲染进程、GPU 进程等子进程

    • 实现崩溃隔离,确保单个标签页崩溃不影响整个浏览器

  4. 安全控制

    • 实施同源策略和 CORS 管理

    • 执行内容安全策略(CSP)

    • 管理网站权限(如地理位置、摄像头访问)

  5. 扩展管理

    • 加载和运行浏览器扩展

    • 控制扩展的权限和生命周期

通过多进程架构,主进程提高了浏览器的稳定性和安全性,即使某个标签页崩溃,也不会影响整个浏览器的运行。

网络进程(Network Process)

在现代浏览器的 多进程架构 中,网络进程 负责 所有网络通信 ,包括 页面加载、文件下载、AJAX 请求、WebSocket 连接 等。它的独立运行有助于 提升性能、安全性和稳定性,防止网络问题影响浏览器的其他功能。

网络进程与主进程的协作流程:

  1. 用户发起请求

    • 用户在 地址栏输入 URL 或点击超链接,浏览器的 主进程 先检查 缓存 是否可用。

    • 若资源存在于缓存,则直接返回,否则向 网络进程 发送请求。

  2. 网络进程处理请求

    • 网络进程负责 DNS 解析、TLS 握手(HTTPS)和 HTTP 请求

    • 获取到资源后,解析 HTTP 头部,判断是否需要预加载其他资源。

  3. 数据返回与解析

    • 资源数据通过 IPC(进程间通信) 传输回 主进程 ,如果资源较大,则可能 流式传输

    • 主进程 根据资源类型 决定如何处理(如 HTML 解析、文件下载等)。

  4. 资源交给渲染进程

    • HTML、CSS、JavaScript 资源被传递到 渲染进程 进行解析和执行。

    • 渲染进程构建 DOM、CSSOM,执行 JS,并生成页面的视觉呈现。

  5. 页面渲染

    • 渲染结果主进程 处理,并最终呈现在 用户界面 中。

除了上述功能之外,网络进程的其他功能主要有以下几个方面:

管理缓存 :包括 HTTP 缓存、Service Worker 缓存,支持离线访问。

优化性能 :支持 资源预加载、流式传输,减少页面加载时间。

增强安全性:独立进程隔离,防止恶意代码通过网络攻击浏览器核心组件。

网络进程通过 独立处理网络任务 ,与 主进程、渲染进程高效协作 ,确保浏览器在 高性能加载网页 的同时 保持安全性和稳定性。🚀

渲染进程

渲染进程的分配与启动

浏览器进程 接收到 网络进程 传输的 HTML 数据后,需要决定如何处理该数据,通常涉及以下两步:

  1. 判断是否需要创建新的渲染进程:浏览器采用 站点隔离(Site Isolation) 策略,决定是 复用现有渲染进程 还是 创建新渲染进程

    • 同站点复用进程 :如果新页面 与当前页面同源 (协议 + 根域名相同),可能 复用 现有渲染进程,减少资源开销,例如:https://example.com/page1https://example.com/page2 可能共用同一个渲染进程。

    • 跨站点创建新进程 :如果用户从 https://example.com 跳转到 https://another-site.com,浏览器会 创建新的渲染进程 以隔离不同站点,提高安全性。

    • 后台标签页优化 :对于 未激活的标签页 ,浏览器可能会 挂起渲染进程 以节省资源,当用户切换回该标签页时,再次恢复进程。

  2. 启动渲染进程:当浏览器决定 创建新的渲染进程 时,浏览器进程会执行以下操作:

    • 启动渲染进程 ,创建独立的 Renderer Process

    • 发送导航请求 ,通过 IPC(进程间通信)HTML 数据、CookiesCORS/CSP 安全策略等信息传递给渲染进程。

    • 渲染进程初始化 ,接收数据后,准备 解析 HTML ,构建 DOM,并执行后续渲染任务。

  3. 渲染进程解析 HTML,构建 DOM 树,加载 CSS 并生成 StyleSheets,执行 布局计算和绘制,最终交由 GPU 进程渲染并显示到屏幕。

浏览器通过 站点隔离策略 确定渲染进程的分配方式,利用 IPC 传输数据 ,渲染进程初始化后 解析 HTML 并执行渲染,最终页面呈现给用户。 🚀

谷歌浏览器的进程间通信(IPC)

网络进程、浏览器进程和渲染进程 之间,谷歌浏览器使用 IPC(进程间通信) 机制进行数据交换,以确保稳定、高效的协作。

常见的 IPC 方式主要有以下几个方面:

  • 管道通信 :浏览器进程与渲染进程通过 操作系统级管道 进行数据传输,适用于 指令传输和资源加载

  • 共享内存 :多个进程访问同一块 共享内存区域 ,适用于 大数据量传输(如视频、图像)。

  • 消息传递 :基于 消息队列 ,进程间通过发送和接收消息交换数据,适用于 小型数据和控制命令 传输。

管道通信流程如下步骤所示:

  1. 创建管道 :浏览器进程与渲染进程在 操作系统级 创建 单向或双向管道

  2. 数据传输 :浏览器进程将 HTMLCSSJavaScript 资源写入管道的 写端 ,渲染进程从 读端 读取。

  3. 数据处理 :渲染进程解析 HTML、执行 JavaScript,计算布局并进行页面渲染。

  4. 返回结果 :渲染完成后,渲染进程将页面内容写入管道的 写端 ,浏览器进程从 读端 获取最终渲染结果并显示给用户。

管道通信是 浏览器进程与渲染进程之间的重要 IPC 方式 ,主要用于 高效传输页面渲染数据 。它依赖 操作系统提供的进程隔离机制 ,确保数据安全,同时支持 大数据量传输 。默认情况下,管道是 单向的 ,若需 双向通信 ,则需要使用 双管道或全双工管道 。除了管道通信,浏览器还使用 共享内存和消息传递 等方式进行进程间通信,以确保 浏览器的稳定性和性能 🚀。

构建 DOM 树

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

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

The tokenization algorithm

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

html 复制代码
<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 树:

在构建 DOM 树的过程中,浏览器采用 逐步增量解析 的方式,即逐个解析 token 并逐步构建 DOM 结构。这一过程是可重入的 ,意味着在处理一个 token 时,解析器可能会被中断,然后在合适的时机恢复,并继续处理后续 token

可重入性对浏览器性能至关重要,特别是在解析 大型 HTML 文档复杂的标记结构 时。它允许浏览器在 DOM 解析尚未完成时 进行渐进式渲染 ,而不必等待整个 DOM 树构建完毕后再进行页面显示。这样不仅能提升 页面的首屏加载速度 ,还允许用户在页面加载过程中进行交互,从而提升整体的 用户体验和响应性

子资源加载

网站通常会使用图片、CSS 和 JavaScript 等外部资源。这些文件需要从网络或缓存加载。主线程可以在解析构建 DOM 时找到它们时逐个请求它们,但为了加快速度,"预加载扫描器"会并发运行。如果 HTML 文档中存在 <img><link> 等内容,预加载扫描器会查看 HTML 解析器生成的令牌,并向浏览器进程中的网络线程发送请求。

当 HTML 解析器找到 <script> 标记时,它会暂停解析 HTML 文档,并必须加载、解析和执行 JavaScript 代码。原因是 JavaScript 可以使用 document.write() 等内容更改文档的形状,而 document.write() 会更改整个 DOM 结构(HTML 规范中的解析模型概览中有个很棒的图表)。因此,HTML 解析器必须等待 JavaScript 运行,然后才能继续解析 HTML 文档。

如果您的 JavaScript 不使用 document.write(),您可以向 <script> 标记添加 asyncdefer 属性。然后,浏览器会异步加载和运行 JavaScript 代码,并且不会阻塞解析。您也可以使用 JavaScript 模块(如果适用)。<link rel="preload"> 是一种方式,用于告知浏览器当前导航确实需要该资源,并且您希望尽快下载。

样式计算

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

CSS 解析

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

  • 通过 link;

  • 引用的外部;

  • CSS 文件;

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

即使您未提供任何 CSS,每个 DOM 节点也都有计算样式。<h1> 标记的显示大小大于 <h2> 标记,并且为每个元素定义了边距。这是因为浏览器具有默认样式表。

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,也就是后面那一步。渲染树包含了页面中需要渲染的节点和其对应的样式信息。

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

css 复制代码
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 是一种用于确定样式规则优先级的机制。它决定了在多个样式规则应用到同一个元素时,哪个规则将具有最终的样式效果。

      1. HTML 中,元素的 <style> 属性的值是样式表规则。这些规则没有选择器,所以 a=1, b=0, c=0, d=0;

      2. 计算选择器中 ID 属性的个数,也就是 a=0, b=1, c=0, d=0;

      3. 计算选择器中其他属性和伪类的数量,也就是 a=0, b=0, c=1, d=0;

      4. 计算选择器中元素名称和伪元素的数量,也就是 a=0, b=0, c=0, d=1; 请看下面的例子,如下所示:

      css 复制代码
       *             {}  /* a=0 b=0 c=0 d=0 -> specificity = 0,0,0,0 */
      li            {}  /* a=0 b=0 c=0 d=1 -> specificity = 0,0,0,1 */
      li:first-line {}  /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */
      ul li         {}  /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */
      ul ol+li      {}  /* a=0 b=0 c=0 d=3 -> specificity = 0,0,0,3 */
      h1 + *[rel=up]{}  /* a=0 b=0 c=1 d=1 -> specificity = 0,0,1,1 */
      ul ol li.red  {}  /* a=0 b=0 c=1 d=3 -> specificity = 0,0,1,3 */
      li.red.level  {}  /* a=0 b=0 c=2 d=1 -> specificity = 0,0,2,1 */
      #x34y         {}  /* a=0 b=1 c=0 d=0 -> specificity = 0,1,0,0 */
      style=""          /* a=1 b=0 c=0 d=0 -> specificity = 1,0,0,0 */

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

布局

现在,渲染程序知道文档的结构以及每个节点的样式,但这还不足以呈现网页。假设您正尝试通过电话向朋友描述一幅画。"有一个大红圈和一个小蓝方块"的信息不足以让您的朋友知道画作具体是什么样子。

布局是查找元素几何图形的过程。主线程会遍历 DOM 和计算样式,并创建包含 xy 坐标和边界框大小等信息的布局树。布局树的结构可能与 DOM 树类似,但它仅包含与网页上显示的内容相关的信息。如果应用了 display: none,则该元素不属于布局树(不过,具有 visibility: hidden 的元素属于布局树)。同样,如果应用了包含 p::before{content:"Hi!"} 等内容的伪元素,即使该元素不在 DOM 中,也会包含在布局树中。

分层

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

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

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

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

  1. 创建图层 :根据 布局树样式信息 ,浏览器会将页面内容划分为不同的图层。通常,具有 复杂渲染 需求的元素(如动画、视频、3D 变换等)会被单独划分为一个图层,以优化绘制效率,减少页面整体重绘的影响。

  2. 图层绘制 :每个图层都有 独立的绘制表面 ,浏览器会先将图层中的内容绘制到 离屏缓冲区 (Offscreen Surface)。这样,当图层发生变化时,只需要重新绘制该 图层,而不必重绘整个页面,从而提升渲染效率并减少资源消耗。

  3. 图层合成 :浏览器通过 合成线程 将各个图层的绘制结果合并,生成最终页面显示。合成过程中涉及 透明度混合、滤镜应用 等操作,并且通常由 GPU 硬件加速 执行,以提高渲染性能并减少主线程的负担。

  4. 分层优化 :浏览器会根据 图层的可见性、变化频率硬件资源 进行优化。例如,避免不必要的重绘、动态调整图层策略,并合理利用 GPU 资源,以确保页面渲染流畅且高效。

通过分层,浏览器可以实现更高效的页面渲染和交互体验。分层技术不仅能 提高绘制速度、减少重绘范围 ,还能 降低内存占用 ,充分利用 硬件加速,从而提升动画流畅度和交互效果。

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

如下图所示,它是主线程遍历布局树并生成层次结构:

图层绘制

在完成 图层树构建 后,渲染引擎会对每个图层进行绘制。接下来,我们看看渲染引擎是如何执行 图层绘制 的。

浏览器会将 每个需要绘制的元素及其样式信息 转换为 绘制记录 ,其中包含 绘制命令和相关参数 。这些绘制记录会被存储在 绘制列表(Draw List)中,等待后续处理。

为了确保正确的 绘制顺序 ,浏览器会根据 绘制记录的层叠关系 对绘制列表进行排序。通常,位于上层的元素会覆盖下层的元素,以确保最终的页面呈现符合层叠规则。例如,一个绘制列表可能如下所示:

最终,在 图层绘制阶段 ,渲染引擎的输出结果就是 这些排序后的绘制列表 ,并将其提交给 合成线程 进行进一步处理。

栅格化操作

绘制列表仅用于 记录绘制顺序和绘制指令 ,并不会直接执行绘制操作。真正的绘制工作由 渲染引擎的合成线程 负责执行,它根据绘制列表 逐步处理每个图层的绘制任务,最终将页面渲染到屏幕上。

在谷歌浏览器中,渲染主线程合成线程 共同协作,完成页面的渲染和显示。它们的具体职责如下:

  • 绘制命令传递 :渲染主线程负责 生成绘制命令 ,并将绘制内容(包括 绘制记录、栅格化结果、纹理数据 等)传递给合成线程。

  • 图层合成 :合成线程接收 渲染主线程传递的绘制内容 ,根据 图层的层叠关系和样式属性 进行 图层合成,将多个图层的绘制结果混合、合并,生成最终的页面图像。

  • 纹理上传 :合成线程将合成后的 纹理数据 上传至 GPU,利用 硬件加速 提高渲染效率。

  • 显示刷新 :合成线程将合成后的 图像提交至显示设备 ,并与 显示刷新率同步 ,确保页面内容按 固定时间间隔 进行 平滑更新

这种 线程协作机制 能够大幅提升页面的渲染性能,减少 主线程阻塞 ,确保 流畅的动画和交互体验。 🚀

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

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

绘制指令完成后,下一步是光栅化,这个过程将绘制指令转换为位图(像素)。这一步通常在 GPU 上进行,因为 GPU 擅长处理并行计算,能够快速完成大量像素的计算和渲染。

光栅化(Rasterization) 是浏览器渲染流程中的 关键步骤 ,用于将网页的各个图层(包括 文本、图片、CSS 效果 等)转换为 位图(bitmap) 。位图由 像素网格 组成,每个像素包含 特定的颜色值 ,使得 显示设备(如计算机屏幕) 能够直接渲染和显示页面内容。

渲染进程维护了一个光栅化的线程池,所有的图块光栅化都是在线程池内执行的,运行方式如下图所示:

通过 光栅化线程池 ,渲染进程能够 并行执行栅格化任务 ,充分利用 多核处理器 提高 光栅化效率和速度 。这种优化对于 复杂页面、大量矢量图形和动画效果 特别重要,有助于 减少渲染延迟 ,提升 用户体验

光速栅格化是一种 优化算法 ,旨在 提高光栅化速度 ,减少 不必要的计算,通常采用以下技术:

  • 并行计算 :利用 SIMD 指令GPU 计算,并行处理多个像素,加速栅格化过程。

  • 剔除和裁剪 :去除 不可见的像素区域,避免无效计算和绘制,提高渲染效率。

  • 低分辨率栅格化 :对于 远处或较小的对象 ,使用 低分辨率光栅化 以减少计算量,同时保持视觉质量。

光栅化的主要步骤如下所示:

  1. 准备阶段 :浏览器的渲染引擎 确定需要光栅化的元素 ,这一过程通常依赖 页面布局和图层分解的结果

  2. 向量图形到位图的转换 :页面中的 文本、SVG 图形、CSS 生成的形状 最初是以 向量形式 描述的,光栅化的任务是将这些向量形状 转换为位图,并计算每个像素的颜色值。

  3. 文本渲染 :文本的光栅化需要 确保清晰度和可读性 ,涉及 字体选择、大小调整、抗锯齿处理 等优化。

  4. 图像处理 :对于 JPEGPNG已经是位图格式的图片 ,通常不需要转换,但可能会进行 缩放、滤镜应用或其他图像处理

  5. 应用 CSS 效果 :在光栅化阶段,浏览器会对 阴影、模糊、滤镜等 CSS 效果 进行处理,这些效果会影响元素本身及其周围像素的颜色和亮度。

创建图层树并确定绘制顺序后,主线程会将这些信息提交给合成器线程。然后,合成程序线程会对每个图层进行光栅化处理。图层可能很大,例如整个网页的长度,因此合成程序线程会将其划分为图块,并将每个图块发送到光栅线程。光栅线程会对每个图块进行光栅化处理,并将其存储在 GPU 显存中。

合成和显示

当所有 图块(Tiles) 完成光栅化后,合成线程 生成 Draw Quad ,用于描述页面元素的 绘制信息 。在谷歌浏览器的渲染流程中,Draw QuadCompositor Frame 共同完成 绘制指令的组织与最终的渲染结果

Draw Quad渲染引擎的绘制指令 ,用于定义页面中 各个图层的绘制方式。它包含:

  • 位置 & 大小:描述元素在页面上的坐标和尺寸。

  • 颜色 & 纹理坐标:指定对象的视觉属性,如背景颜色、纹理等。

  • 绘制目标 :可以是 HTML 元素、图片、视频等 ,一个元素可能对应多个 Draw Quad

Compositor Frame合成线程最终处理后的完整渲染帧 ,包含所有 已排序的 Draw Quads ,用于提交至 浏览器进程 & GPU 进行渲染:

  • 由多个 Tiles(图块) 组成,每个 Tile 代表页面的一部分内容。

  • 合成线程处理 Draw Quads,根据 层叠关系、透明度和样式特效 生成 Compositor Frame,以确保最终渲染效果。

它的渲染流程如下步骤所示:

  1. 渲染进程合成线程 生成 Draw Quad,用于描述页面元素的绘制信息。

  2. 合成线程 处理 Draw Quad,按照 层级关系 进行排序,生成 Compositor Frame

  3. 通过 IPCCompositor Frame 提交至 浏览器进程,界面线程(UI Thread)可添加 UI 变更(如滚动条更新)。

  4. 最终 Compositor Frame 发送至 GPU ,通过 纹理上传 & 硬件加速 进行渲染,并根据 VSync(垂直同步) 控制刷新显示。

  5. 若发生滚动或交互事件 ,合成线程可能会 创建新的 Compositor Frame 适应 UI 变化,并再次提交到 GPU 进行渲染。

如下图所示:

GPU 处理后的图像会存入 帧缓冲区(Frame Buffer),这是显示设备用于存储即将显示的图像数据的缓冲区。浏览器通常使用双缓冲技术,即:

  1. 前缓冲区(Front Buffer) 存放当前正在显示的内容。

  2. 后缓冲区(Back Buffer) 存放下一帧渲染的内容,准备切换显示。

浏览器等待 显示设备的刷新信号(VSync),同步屏幕的 刷新频率(通常是 60Hz,即每 16.67ms 刷新一次)。VSync 触发后,前后缓冲区交换,新的 Frame Buffer 进入 显示设备,最终用户才能看到更新后的页面内容。

总结

浏览器在解析 URL 后,会先通过 DNS 解析 获取服务器 IP,然后与服务器建立 TCP 连接 并发起 HTTP 请求 获取网页资源。接着,渲染进程 解析 HTML 构建 DOM 树,解析 CSS 生成 StyleSheets,并结合计算样式后进行 布局计算 ,确定元素的 几何位置 。随后,浏览器对页面进行 分层处理 ,将不同层次的元素绘制到 绘制列表 中,并通过 光栅化 将内容转换为位图。最终,合成线程 生成 Compositor Frame,通过 GPU 进行 合成渲染 ,并与 VSync 同步 将页面内容显示到屏幕上,实现 流畅的页面呈现与交互 🚀。

相关推荐
阿古达木几秒前
从零开发设计稿转代码插件(二)
前端
爱刷牙的鲨鱼辣椒几秒前
简单且非常实用的代码优化技巧-会一直收集并增加
后端
NMBG227 分钟前
[JAVASE] 反射
java·开发语言·jvm·后端·intellij-idea
喵个咪14 分钟前
开箱即用的GO后台管理系统 Kratos Admin - 如何进行Docker部署后端
后端·微服务·go
Emma歌小白16 分钟前
DOT 语言的应用
后端
程序员黄同学1 小时前
解释 TypeScript 中的枚举(enum),如何使用枚举定义一组常量?
javascript·ubuntu·typescript
zhyoobo1 小时前
Spring Boot 性能优化:如何解决高并发下的瓶颈问题?
spring boot·后端·性能优化
一线大码1 小时前
关于 LEFT JOIN 的使用注意事项
后端·sql·mysql
Xlbb.1 小时前
SpiderX:专为前端JS加密绕过设计的自动化工具
前端·javascript·自动化
uhakadotcom1 小时前
uvloop让你的异步代码速度提升400%,实战讲解与代码示例
后端·面试·github