浏览器运行机制

进程和线程

进程和线程是操作系统中管理和执行任务的基本单位。它们有不同的概念和作用,理解它们之间的区别对于编程和系统设计非常重要。

  • 进程(process):程序的一次执行过程,是程序在执行过程中操作系统分配资源和管理任务的基本单位;每个进程都拥有自己独立的内存空间和系统资源,进程之间无法直接访问彼此的内存,这也意味着一个进程的崩溃不会直接影响到其他进程。
  • 线程(thread)是 CPU 调度和分派的基本单位,它可与同属一个进程的其他的线程共享进程所拥有的全部资源,但彼此独立执行。

浏览器的多进程

浏览器可以视作一个复杂的应用程序。当用户启动浏览器时,操作系统会为该应用程序分配一个进程。这个进程启动后,CPU 会为其分配相应的内存空间,供其运行和存储数据。进程启动后,便可以利用线程来执行特定任务,从而实现应用程序的功能。

在一个应用程序中,单一进程可能无法满足所有的功能需求,因此它会创建新的进程来处理其他任务。这些新创建的进程与原有进程是相互独立的,它们各自拥有独立的内存空间,无法直接共享数据。这种隔离增强了应用程序的安全性和稳定性。

尽管这些进程不能直接共享内存,它们仍然需要协同工作。为此,操作系统提供了 IPC(Inter Process Communication,进程间通信)机制,允许不同的进程之间进行数据交换和协调操作。通过 IPC,进程可以相互发送消息、共享资源或同步操作,从而实现复杂的应用逻辑。

Chrome 浏览器作为一种多进程架构的浏览器,通过将不同的任务分配到不同的进程和线程来提高性能、安全性和稳定性。以下内容以 Chrome 为例。

Chrome 的多进程架构包括以下主要进程:

  • 浏览器主进程 (Browser Process):
    • 负责管理浏览器的用户界面(地址栏、书签栏)的工作、处理控制逻辑(标签页管理,前进后退等)、网络请求的调度和文件访问与操作系统的交互等。
    • 主进程唯一,协调各个子进程,管理 Chrome 的整体运行。
  • 渲染进程 (Renderer Process):
    • 负责处理网页内的显示相关的工作(HTML、CSS 解析和布局、JS 执行等),也称渲染引擎。
    • 每个标签页通常拥有独立的渲染进程(多个标签页可能会共享同一个进程,取决于内存使用情况)。通常运行在沙箱模式下。
  • GPU进程 (GPU Process):
    • 负责处理整个应用程序的 GPU 任务(CSS3 渲染、WebGL、Canvas、视频解码等操作)。常用于 3D 图形处理,硬件加速任务。
    • GPU 进程和渲染进程是单独的进程,它们之间通过 IPC 进行通信。这样的好处是在不同的渲染进程之间可以共享 GPU 资源,提升图形处理性能。
  • 插件进程 (Plugin Process):
    • 负责控制网页使用到的插件。
    • 插件进程也是独立的(与渲染进程隔离),确保不会因为插件崩溃影响页面。
  • 拓展进程 (Extension Process):
    • 负责管理 Chrome 拓展程序的执行。
  • 网络进程 (Network Process):
    • 负责处理所有网络相关操作,HTTP/HTTPS 请求、WebSocket、缓存等。

浏览器地址栏中输入 URL 浏览器的执行操作

  1. URL 解析:将其分解为协议、域名、路径、查询参数和锚点等部分。并且检查缓存。
  2. DNS 解析:如果没有缓存命中,浏览器需要将域名(如 www.example.com)转换为 IP 地址。浏览器首先检查本地 DNS 缓存,若未找到,则会向本地 DNS 服务器发起 DNS 查询。
  3. 建立 TCP 连接:浏览器会向 DNS 服务器发起请求,获取到 IP 地址后,会向 IP 地址对应的服务器发起 TCP (三次握手,SYN + SYN-ACK + ACK)连接。如果是 https 协议,还会进行 TLS/SSL 握手,确保数据加密。
  4. 发送 HTTP 请求:浏览器会向服务器发送 HTTP 请求,请求包含请求行、请求头和请求体。
  5. 接收响应:浏览器接收服务器返回的 HTTP 响应报文,并根据状态码判断请求是否成功(如 200 OK 表示成功,404 表示资源未找到)。之后开始解析 HTML 文档,构建 DOM 树。处理 CSSOM 树和 JS 文件,构建渲染树。
  6. 页面渲染:浏览器会根据 DOM 树和 CSSOM 树,构建渲染树,并开始渲染。浏览器会根据渲染树,将页面渲染到屏幕上。这包括布局(计算元素的大小和位置)、绘制(将元素渲染到屏幕)和合成(对页面内容进行最终的图形处理)。

浏览器处理和解析返回的资源和数据

渲染进程几乎负责标签页内的所有事情,渲染进程的核心目的在于转换 HTML、CSS 和 JS 为用户构建可交互的 web 页面。渲染进程中主要包含以下线程:

  1. 主线程 Main thread
  2. 工作线程 Worker thread
  3. 排版线程 Compositor thread
  4. 光栅线程 Raster thread

渲染流程:

  1. 构建 DOM 树。
  2. 加载次级资源:网页中包含诸如图片,CSS,JS 等额外的资源,这些资源需要从网络上或者缓存中获取。主进程在构建 DOM 的过程中会逐一请求它们。
  3. JS 的执行和下载:当遇到 <script> 标签时,渲染进程会停止解析(1)(2) HTML,而去加载,解析和执行 JS 代码,停止解析 html 的原因在于 JS 可能会改变 DOM 的结构。
  4. 样式计算:仅仅渲染 DOM 还不足以获知页面的具体样式,主进程还会基于 CSS 选择器解析 CSS 获取每一个节点的最终的计算样式值。即使不提供任何 CSS,浏览器对每个元素也会有一个默认的样式。
  5. 获取布局:想要渲染一个完整的页面,除了获知每个节点的具体样式,还需要获知每一个节点在页面上的位置,布局其实是找到所有元素的几何关系的过程。其具体过程如下:通过遍历 DOM 及相关元素的计算样式,主线程会构建出包含每个元素的坐标信息及盒子大小的布局树。布局树和 DOM 树类似,但是其中只包含页面可见的元素,如果一个元素设置了 display:none ,这个元素不会出现在布局树上,伪元素虽然在 DOM 树上不可见,但是在布局树上是可见的
  6. 绘制元素:即使知道了不同元素的位置及样式信息,我们还需要知道不同元素的绘制先后顺序才能正确绘制出整个页面。在绘制阶段,主线程会遍历布局树以创建绘制记录。绘制记录可以看做是记录各元素绘制先后顺序的笔记。
  7. 合成帧

(1). 渲染线程与 JS 引擎线程是互斥的,所以渲染过程中,如果遇到 <script> 就停止渲染,执行 JS 代码,也就是说,在构建 DOM 时,HTML 解析器若 遇到了 JS ,那么它会暂停构建 DOM,将控制权移交给 JS 引擎,等 JS 引擎运行完毕,浏览器再从中断的地方恢复 DOM 构建。如果需要首屏渲染加快,就不应该在首屏就加载 JS 文件,可以将 <script> 放在 <body> 最底部。

(2). 如果不希望 <script> 阻塞 DOM 的构建,可以使用 defer 或者 async:

  • defer 表示延迟执行,在 document 解析完毕且 defer-script 也加载完成之后(异步执行)会执行所有 defer-script 代码,然后触发 DOMContentLoaded 事件。
  • async 顾名思义将 script 异步加载,与 defer 不同,async 加载完成后会立刻执行。在加载时不会阻塞,执行代码时才阻塞。

浏览器的重绘与重排

在浏览器中,重绘(Repaint) 和 重排(Reflow)(又称回流)是两个重要的渲染过程,它们在页面的更新和显示过程中起着关键作用。

  1. 重排(Reflow):浏览器重新计算页面的布局。当页面的结构(如元素的尺寸、位置或其他属性)发生变化时,浏览器需要重新计算元素的位置和几何信息,这个过程就是重排。重排通常由以下操作触发:
  • 添加或删除可见的 DOM 元素。
  • 改变元素的尺寸(宽度、高度、边距、边框、内边距等)。
  • 改变元素的显示状态(如 display 属性)。
  • 改变元素的位置(如 position 属性)。
  • 改变元素的内容(如文字、图片等)。
  • 改变页面的布局属性(如 width、height、font-size 等)。
  • 通过 JavaScript 访问某些需要布局计算的属性(如 offsetWidth、offsetHeight、scrollTop 等)。

重排是一个代价昂贵的操作,因为它会导致浏览器重新计算页面上所有或部分元素的布局。当页面中的元素较多或变化频繁时,重排会显著影响页面性能。

  1. 重绘(Repaint):浏览器在元素的视觉部分发生改变(如颜色、背景、阴影等)时,需要将这些变化反映到屏幕上。重绘是一个较轻量的操作,相较于重排,其代价要小很多,因为不需要重新进行布局计算。重绘通常由改变元素的样式而不影响其几何属性的操作触发:(如 color、background-color、visibility、border-color、box-shadow、outline 等)。

重绘仅会重新绘制受到影响的部分,不会改变文档的布局,因此代价相对较小,但频繁的重绘仍会影响性能。

重排通常会导致重绘,重新计算布局后,浏览器会重新绘制受影响的区域。但重绘不会导致重排。

为了提高页面的性能,应尽量减少重排和重绘的次数,尽量使用 CSS 来控制元素的样式,避免在 JavaScript 中修改样式。以下是一些优化思路:

  • 减少布局更改:批量修改 DOM 树,而不是进行多次小修改。将布局操作完成后再插入。
  • 避免重排:使用 CSS 的定位、浮动、固定定位等属性,避免元素在布局中发生位置改变,缓存 offsetWidth,scrollTop 等属性,减少直接访问次数。
  • 优先使用不会导致重排的 CSS 属性来实现过渡和动画:例如使用 translate 来替换 top/left 等定位操作。translate 可以由 GPU 处理,不会触发重排。
  • 减少复杂的选择器: 过于复杂的选择器会影响样式计算性能。