在探索 EventLoop 的过程中,我偶然发现了一篇深入剖析浏览器内部机制的文章:inside-browser(chrome),其中对各个阶段的过程描述得非常详尽和透彻。在仔细研读完这篇内容之后,我对于如何应对那些“在浏览器输入 URL 后发生了什么?”的面试题充满了信心,希望能够游刃有余地解答这类问题。
浏览器进程的线程结构:UI 线程,Network 线程,GPU 线程
以 Chrome 浏览器为例,当我们启动浏览器时,会创建一个 Browser 浏览器进程,该进程内部会启动多个关键线程,包括 UI 线程、Network 线程 等。其中,UI 线程 负责处理用户界面相关的操作,而 Network 线程 则负责处理用户输入的 URL 地址,执行页面跳转以及后续的资源加载任务。
渲染进程的线程结构:Main 线程,Compositor 线程,Raster 线程
页面的渲染工作是通过 Renderer 渲染进程 完成的,整个渲染过程由 Main 主线程 主导,并涉及到 Compositor 线程 和 Raster 线程 的协同工作。下面,我们将针对 在浏览器输入 URL 后发生了什么? 这一核心问题,详细列举几个关键步骤:
当浏览器启动后,会创建一个 Browser 进程。用户在地址栏中输入 URL 后,浏览器的 UI 线程(UI thread) 会解析输入的内容,判断输入的是一个 URL 还是普通文本,并据此决定后续的操作流程。
如果用户确认输入的是一个 URL 并按下回车键,浏览器会开启一个新的 tab 标签页。此时,网络线程(network thread) 将开始工作,首先进行 DNS 查询,然后建立 TLS 传输通道,为后续的数据传输做好准备。
根据服务器返回的 Content-Type 响应头信息,浏览器会判断该资源属于哪种媒体类型,例如 text/html、image/png 等。同时,浏览器还会启动安全监测机制,对用户访问的链接进行风险评估,对于潜在的危险链接会给出警告提示:
在页面中,我们经常会使用 script 和 img 等元素引用来自不同域名的资源。为了确保用户的安全,浏览器提供了 CORB(Cross-Origin Read Blocking)政策,对这些跨域资源进行安全监测。
当浏览器的安全检查工作完成后,网络进程 将知道哪些资源需要加载,UI 线程 会主动寻找可用的 渲染进程(renderer thread)。在资源加载的过程中,UI 线程 会通过 IPC(Inter-Process Communication)机制将返回的数据同步通知给 Renderer 渲染进程。当 渲染进程 完成页面的渲染工作后,会通知 Browser 进程 完成导航跳转。
Renderer 渲染进程 的工作流程相对复杂,整体过程可以概括为以下几个步骤,下面我们将逐一进行详细解释。
第5步:解析 DOM Tree,加载外部依赖
在 Browser 进程 中,通过 网路线程 获取到的页面资源——HTML,会被发送给 Renderer 渲染进程。Renderer 进程 会通过 主线程 按照 HTML 标准规范将 HTML 内容转化为 DOM 文档对象模型。
在解析过程中,如果遇到 link 或 script 标签,网络线程 会访问所引用的文件。如果这些文件需要较长时间加载,或者 JavaScript 代码执行时间较长,将会导致 DOM 解析被阻塞,从而出现“页面白屏”的现象。第6步:计算样式
加载完 CSS 文件后, 主线程 会将 DOM 中的节点和样式进行匹配,生成 Computed Style 。
第7步:流式布局 Flow
仅仅知道 DOM 节点数和样式信息还不足以渲染页面, 主线程 会根据这两者继续生成 LayoutBlock Flow,
多个LayoutBlock Flow组成了LayoutTree 渲染树。Layout 渲染树 可能与DOM 树结构相似,但由于 display:none 或一些 CSS 伪元素的 content 定义,Layout 渲染树 与 DOM 树 存在差异。不同的 LayoutBlock Flow 会根据当前元素的 几何属性 确定它们之间的相对位置,从而在从上至下的排序中,调整好布局方式。
第8步:绘制 Paint
即使有了 Layout 渲染树,仍然无法直接展示页面,还需要对每个 LayoutBlock Flow 布局的流块进行进一步处理,包括各种颜色、position 坐标、通过 z-index 定义的图层、绘制的文字等信息。最终,LayoutBlock Flows 会被转化为 Paint Records 绘制记录,以便在 合成环节 中使用。
第9步:合成 Compositing
我们在代码中定义的 px,屏幕并不能直接理解,需要通过一套规则将其转换为计算机能够识别的形式。这套规则就是 合成:页面将被分割为多个 layers 图层 进行渲染,每个图层由 compositor thread 印刷线程 解析 Paint Records 绘制记录 进行光栅化(将矢量形状转换为位图形式)处理后的内容组成。如果页面发生滚动,将重新对图层进行光栅化。为了得到 layer 图层 , 主线程 需要将 Layout Tree 转化为** Layer Tree 图层树**:
一旦上述工作完成后,主线程 会告知 compositor 排版线程,它会将内容交给 raster 线程 进行光栅化(rasterizes),然后交给 GPU 处理。
处理完这些内容(tiles)后, compositor 线程 将创建 compositor frame 排版帧,帧内的内容称为 draw quads 绘制好的四方格。最后, compositor 线程 会通过 IPC 告知 Browser 进程 ,将 draw quads 交给 UI线程 ,以及 GPU 线程 。我们的屏幕会根据 60fps 刷新并展示所接收到的排版帧,页面的样子就一点点渲染出来了。
这张图是在搜索“回流,重绘”概念文章中 频繁出现 的“老面孔”,相信在了解上述几步的运行机制后,您会对这张图有更深刻的理解。
最后,我们提出几个小问题,希望能够帮助大家触类旁通:
- 为什么 html 和 css 的解析是分开的?
- DOM 树 和 Style 样式 合成什么东西?
- 布局,绘制内部到底在做什么?
- 最后屏幕上的展示内容又是怎么呈现在我们的面前?
- Inside look at modern web browser (part 2): https://developer.chrome.com/blog/inside-browser-part2/
- Inside look at modern web browser (part 3): https://developer.chrome.com/blog/inside-browser-part3/
- Cross-Origin Read Blocking for Web Developers: https://www.chromium.org/Home/chromium-security/corb-for-developers/
- Eliminate content repaints with the new Layers panel in Chrome – LogRocket Blog: https://blog.logrocket.com/eliminate-content-repaints-with-the-new-layers-panel-in-chrome-e2c306d4d752/?gi=cd6271834cea
- Stick to Compositor-Only Properties and Manage Layer Count: https://web.dev/stick-to-compositor-only-properties-and-manage-layer-count/