入门小知识

何为进程

程序运行需要开辟专属于自己的内存空间, 这部分可以简单理解为进程。

进程 是程序的实体。

进程 是系统进行资源分配的基本单位, 是操作系统结构的基础。

进程线程 的容器。

每个应用都至少有一个进程。进程之间相互独立, 进程之间需要通信需要双方同意。

何为线程

线程是OS能够进行运算调度的最小单位。

它是进程中运行代码的实际单位。

一个进程至少有一个线程, 所以在进程开启后 会自动创建一个线程来运行代码, 该线程称之为主线程。

如果程序需要同时执行多块代码, 主线程就会启动更多的线程来执行代码, 所以一个进程中可以包含多个线程。

抽象理解

可以把一个应用(进程)比作一个产品工厂

这个工厂会有一个厂长 → 主线程, 主线程负责把控方向, 调度人员, 完成一些高难度的、主要的任务

一个生产产品的工厂只有一个厂长肯定是不行的, 所以厂长就请了很多的员工, 并未不同的员工分配不同的工作

这也就是 主线程 创建新的线程, 新的线程可以执行其他的任务(子线程)




浏览器

浏览器是一个 多进程 多线程 的应用程序

谷歌浏览器里, 通过右击顶部的标签栏, 可以打开任务管理器 Shift + Esc

Windows 也可以通过任务管理器查看

一个pid就是一个进程

Chrome 目前采用的是 SOA 架构, 主要特点是将应用程序的不同的 Service 进行拆分, 并通过这些服务之间定义良好的接口和协议联系起来。

浏览器的主要进程

  1. 浏览器进程

    主要负责界面展示, 用户交互, 子进程管理等。浏览器进程内部会启动多个线程处理不同的任务。

  2. GPU进程

    如果有GPU, 就会创建GPU进程, 用于加速后面的页面渲染。其他进程/线程不能直接调用GPU, 得通过GPU进程调用

  3. 网络进程

    主要负责处理网络资源加载, 请求响应, 校验 CORS。网络进程内部也会启动多个线程来处理不同的网络任务。

  4. 存储进程

    为 local/session storage, service worker, indexed_db 提供存储服务。

  5. 渲染(Renderer)进程**(重点)**

    负责页面的渲染和 JavaScript 脚本的执行。

    渲染进程启动后, 会开启一个渲染主线程, 主线程负责执行 HTML、CSS、JS 代码。

    默认情况下, 浏览器会为每个标签页开启一个新的渲染进程, 以保证不同的标签页之间不相互影响, 无关乎是否为 same-site 站点(目前)。

    将来该默认模式可能会有所改变, 更多参见 chrome官方说明文档

    同时 V8 和 Blink 也都运行在该进程中。

  6. Service Worker 服务工作线程, 属后台脚本

  7. 插件进程

    负责插件运行, 根据插件的功能决定是否运行在 Sandbox 环境

渲染进程

渲染主线程

主要工作内容

渲染主线程是浏览器中最繁忙的线程, 需要它处理的任务包括但不限于:

  • 执行全局 JS 代码
  • 解析 HTML
  • 解析 CSS
  • 计算样式
  • 布局
  • 处理图层
  • 每秒把页面画 60 次(页面刷新的帧率)
  • 执行事件处理函数
  • 执行计时器的回调函数
  • ……

如何调度执行

比如:

  • 我正在执行一个 JS 函数, 执行到一半的时候用户点击了按钮, 我该立即去执行点击事件的处理函数吗?
  • 我正在执行一个 JS 函数, 执行到一半的时候某个计时器到达了时间, 我该立即去执行它的回调吗?
  • 浏览器进程通知我“用户点击了按钮”, 与此同时, 某个计时器也到达了时间, 我应该处理哪一个呢?
  • ……

于是就有了这样一个 事件循环 的模型, 任务通过排队来交给渲染主线程来执行。

事件循环

为什么渲染进程不适用多个线程来处理这些事情

浏览器渲染进程通常是单线程的, 这是因为多线程处理可能会引发很多问题。以下是一些可能的问题:

  • 竞态条件:多线程处理会导致访问共享内存的竞争条件, 可能导致数据不一致和死锁等问题。
  • 同步问题:多线程需要进行同步, 避免数据竞争和死锁, 这会增加代码的复杂度和开销。
  • 安全问题:多线程可能会存在安全漏洞, 如数据泄露、内存溢出等问题。
  • 性能问题:多线程处理可能会导致过多的上下文切换和内存消耗, 从而降低程序的性能和稳定性。

相比之下, 单线程处理有以下优点:

  • 简单易用:单线程的处理方式更加简单易用, 开发人员不需要考虑多线程处理中的竞态条件、同步问题和安全问题。
  • 可靠稳定:单线程处理避免了多线程处理中的死锁和资源争用等问题, 从而提高了程序的可靠性和稳定性。
  • 高效节省:单线程处理可以避免多线程处理中的上下文切换和内存消耗等问题, 从而提高了程序的性能和节省了系统资源。

虽然单线程处理可能会存在一些缺点, 如无法充分利用多核CPU等问题, 但是它是目前浏览器渲染进程的常用处理方式, 可以通过事件循环和异步编程等技术来提高程序的性能和并发处理能力。

参考: 为什么渲染进程不适用多个线程来处理事情

事件循环

定义

W3C的官方称法是: Event Loop

Chrome 的称法是: Message Loop

This task source is used when an algorithm requires a microtask to be queued.

下面整个 执行 的过程, 称为事件循环。

执行

  1. 在最开始的时候, 渲染主线程 会进入无限循环
  2. 每一次循环都会按优先级检查 消息队列 是否存在任务
    • 若队列不为空, 则取出第一个任务, 并交给渲染主线程进行执行, 执行完之后进入下一次循环
    • 若队列为空, 则进入休眠状态
  3. 其他的所有线程, 包括其他进程的线程, 都可以随时向 消息队列 的末尾添加任务
    • 若主线程为休眠状态, 则会进行唤醒

消息队列

事件循环需要有一个或多个 任务队列 以及一个 微任务队列 , 任务队列是一组任务。

任务队列是 有序集 , 不是队列。

因为 事件循环处理模型 是从所选队列中获取第一个可运行的任务 , 而不是使第一个任务出队。

微任务队列不是任务队列。

来源: W3C - HTML Standard

队列类型

  • 微队列(必须有) 优先级(最高): 用户存放需要最快执行的任务
  • 交互队列 优先级(最高): 用于存放用户操作后产生的事件处理任务
  • 延时队列 优先级(中): 用于存放计时器到达后的回调任务
  • ……

添加任务到微队列的主要方式主要是使用 Promise、MutationObserver

根据 W3C 官方的解释, 每个任务有不同的类型, 同类型的任务必须在同一个队列, 不同的任务可以属于不同的队列。
不同任务队列有不同的优先级, 在一次事件循环中, 由浏览器自行决定取哪一个队列的任务。
但浏览器必须有一个微队列, 微队列的任务一定具有最高的优先级, 必须优先调度执行。

其他主要线程

  • 网络线程
  • 计时线程
  • 交互线程
  • 合成线程
  • ……

网络进程

优点

  • 代码简化:无论请求来自哪个进程, 网络请求都应该以相同的方式发出。同样, 如果将来实现移动流程, 调用者不必进行更改。

  • 性能:mojo 服务可以从任何线程调用, 从而减少线程跳数。它还可以使用 Blink 字符串类型等直接从 Blink 调用, 从而减少转换和层数。

  • 长期来看:使用 mojo 接口并将网络代码与 chrome 代码隔离意味着我们最终可以将网络代码移至单独的进程, 以提高稳定性和安全性。这还允许我们在 Chrome 未运行时(即在带有 ARC++ 的 ChromeOS 上)或在所有平台上(可能来自 Service Worker)而不运行浏览器时从其他 Mojo 应用程序发出网络请求。

参考:

导航篇

解析 URI

当在地址栏输入内容按下回车后, Chrome 首先会解析内容, 判断这是URL还是搜索内容

  • 若是搜索内容则自动URL编码并拼接为默认搜索引擎的 params
    例如谷歌搜索 https://www.google.com/search?q=test
  • 若是URI, 则处理URI, 添加 http 并默认访问 80 端口号
    test.com

img


在 Chrome 层面, 如果你的地址栏原本就有展示页面, 那么进行上述操作后, 会触发当前页面的 beforeunload 与 unload 事件。

同时浏览器标签进入 loading 图标状态。

新页面有两个重要的时间节点, 在渲染篇会详细介绍:

interactive: 它表示浏览器已经完成了 HTML parser 、Recalculate Style、Layout Tree、Render Tree、draw list 等工作。

complete: 它表示浏览器已经完成页面渲染, 这会替换掉本窗口原本的位图, 显示最新的界面。

在 interactive 与 complete 之间, 就是渲染进程中的合成线程的工作位置, Chrome 渲染进程基于 skia 进行 2D 界面元素的绘制。


构建请求

通过 URI Check 后, Chrome 需要为它创建 get 请求

Chrome 主进程需要通过 IPC 把构建请求的任务委托给 NetWork Service 负责此任务。

NetWork Service 接受任务后, 创建了 get 请求, 其中请求行由 请求方法 + 请求路径 + HTTP 版本号 组成;请求头信息由 Chrome 内置提供。

img

查找强缓存

NetWork Service 会委托 Storage Service 依次在

  • service work cache
    • memory cache
      • disk cache
        • push cache(HTTP2 Stream)

中寻找对应的 URI 是否有可用的强缓存, 如果存在强缓存, 则直接使用缓存进入浏览器解析环节, 否则进入 DNS 解析

DNS 解析

若强缓存不存在或过期时, NetWork Service 继续将报文发送至接收端。

这需要 OS 的配合, 首先需要将报文委托给 OS 至协议栈, 但 OS 无法识别报文对应的 domain , 因此无法提供相应帮助。

我们必须提供 IP 地址。将指定域名转换成 IP 的工作是由 DNS 服务器 提供。

DNS 层级
  • 根域 DNS 服务器: 不保存具体的域名信息, 但它是通向所有顶级域 DNS 服务器的总入口
  • 顶级域 DNS 服务器: 代表不同的域名后缀服务器, 如 cn、com、tech 等。同样不保存具体的域名信息, 是通往对应后缀权威 DNS 服务器的总入口
  • 权威 DNS 服务器: 正如其名, 代表着对应 Domain 映射 IP 的权威。它是存储映射关系的真实服务器。

img

Chrome 从 83 版本开始正式开始了 DOH 即 DNS-over-HTTPS
主要目的是防止原本的 DNS 请求因为是 HTTP 明文传输导致容易被中间人篡改, 因此 DOH 就是批着 TLS 的 DNS 请求。

Hosts

域名解析也存在本地定制化入口 Hosts。

它是一个本地的关联 “数据库”, 将 Domain 与 IP 地址相对应。解析优先级大于 DNS 服务。

DNS 解析流程

以访问 http://www.test.com 为例, DNS 的解析流程如下:

  1. 查看 hosts 是否存储目标 domain 和 IP 地址的映射关系

    • 若找到则直接返回给客户端
    • 若 hosts 不存在对应 domain, 客户端建立 DNS 请求, 问询本地 DNS 服务器 Domain 对应的 IP 地址
  2. 本地 DNS 服务器收到请求后, 首先查看 DNS 缓存能否找到 domain 对应的 IP 地址

    • 若找到则直接返回给客户端
    • 若 DNS 缓存中不存在, 则找到自身记录的根域 DNS 地址并发起请求问询根域服务器 Domain 对应的 IP 地址。
  3. 根域服务器不保存具体的数据, 但是指明了我们接下来询问的目标: 对应 com 的顶级域名服务器地址。

  4. 本地 DNS 服务器收到根域的回应后, 继续问询 com 的顶级域名服务器

  5. 顶级域名服务器同理会返回相对应 test.com 的权威服务器地址。

  6. 本地服务器继续问询权威服务器, 它是域名解析结果的原出处, 也是最后一次问询。

  7. 权威 DNS 服务器返回域名对应的 IP 地址给客户端。

  8. 本地 DNS 服务器缓存结果。将 IP 发给 OS。

  9. OS 返回 IP 至 Chrome NetWork Service。

这下 NetWork Service 已经万事俱备。终于可通过 socket library 将数据委托给 OS 以进入协议栈。

同时也标志着即将离开 OSI 应用层。

协议栈

请求数据包在 OS 的帮助下进入协议栈。

工作在应用层与传输层中间的协议栈会处理对应 H2 的 Hpack 和 Stream

如果 domain 使用了 TLS / SSL 协议, 那么 OS 会从本地加密套件列表中选取加密套件, 并将信息添加至数据包。

传输层

至此数据包来到协议簇上层, 它表示工作在传输层和网络层相关的协议总称。

协议簇分为上下两个部分, 分别承担不同的工作且上下层关系有一定的规则。

上层完成部分工作后会委托下层继续执行。

在上层协议簇中最先映入眼帘的便是负责数据包收发的 TCP / UDP

TCP / IP 协议簇下层

TCP 是面向一对一链接, 可靠有状态且基于字节流的协议。

在 HTTP 传输数据之前, 首先需要 TCP 建立连接, TCP 连接的建立, 通常称为三次握手。​

在传输层执行连接、收发、断开等各阶段操作都需要委托 IP 协议将数据包封装成网络包发送给通信对象。

其中最重要的是 源IP目标IP

  • 源IP , 即当前客户端的 IP 地址
  • 目标IP , 即 DNS 域名解析得到的接收端服务器 IP
ARP
  • ARP缓存
  • MAC地址
ICMP

ICMP 协议是 IP 的一个组成部分, 必须由每个IP模块实现。

主要用于在 IP 主机、路由器之间传递控制消息

控制消息是指网络通不通、主机是否可达、路由是否可用等网络本身的消息。

页面渲染

浏览器获取到html文本之后, 会对文本进行解析, 最终将页面呈现在屏幕上

主要是渲染进程里的渲染主线程

解析 HTML - Parse HTML

  1. 预解析

    1. 为了提高解析效率, 浏览器会启动一个 预解析器(预解析线程) 率先下载和解析CSS
    2. 浏览器获得HTML文本之后, 会先快速浏览 需要网络加载的资源, (优先CSS和head中的资源, 也有JS)
      渲染主线程 遇到这些资源的时候, 若 预解析线程 负责就会直接跳过, 而不会阻塞主线程(所以CSS不会阻塞)
    3. 然后 预解析线程 将这些网络资源交给 网络线程
      • 这里的 网络线程渲染进程 里创建线程
      • 后续这里的 网络线程 还会委托 网络进程 NetWork Service 请求资源
      • 得到资源后, 会把资源返回给 预解析线程 进行解析
      • 之后会把解析后的结果交给 渲染主线程 , 由 渲染主线程 生成 CSSOM 树

    预解析 - CSS

  2. 渲染主线程 遇到 JavaScript/script标签 时, 必须停止一切行为, 执行 JS 代码, 若需要下载, 则等待预解析线程的返回

    • 若没下载完成, 则等待下载完成
    • 若预解析线程下载完成, 则直接返回给 渲染主线程 执行代码

    之所以需要停止一切行为来执行 JS 代码, 是因为有可能存在 对在 JS 代码之前 已解析的DOM进行操作
    预解析 - JS

  3. 渲染主线程 会将文本形式的HTML转换成可操作的对象, 并生成 DOM 树和 CSSOM 树

    • DOM 树: Document Object Modle, 控制台通过 documentconsole.dir(document) 输出查看
    • CSSOM 树: CSS Object Modle, 控制台通过 document.styleSheets 输出查看

    DOM Tree
    CSSOM Tree

样式类别

  • 内部样式: <style>
  • 外部样式: <link>
  • 内联样式表/行内样式表: <div style="">
  • 浏览器默认样式表 (F12查看元素样式, 其中的 User agent Stylesheet )

一个样式表(上面都是不同的样式表), 对应 CSSOM 树 中的一个 CSSStyleSheet

样式计算 - Recalculate Style

CSS属性值的计算过程(层叠、继承、优先级等) 在其他md里呈现: [CSS 属性计算过程](./CSS 属性计算过程.md)
视觉格式化模型(盒模型、包含块)

这一步的目的, 是为了得到每个节点的最终样式(计算后的样式, computed style)。

每一个节点的所有CSS属性都要有值, Chrome 打开 F12 选中元素之后, 打开样式, 有个计算样式(Computed), 显示全部(Show All),
可以看到这个元素所有的CSS属性值

在这一过程中, 很多 预设值 会变成 绝对值, 相对单位 会变成 绝对单位 , 比如(字体、宽高等)大小:

  • color: red; 会变成具体的 rgb(255,0,0)
  • em会变成px

(但%是计算不了的, 得要到 布局 - Layout) 阶段 知道相对的 包含块 之后才可以进行计算)

渲染主线程 会遍历得到的 DOM 树, 依次为树中的每个节点计算出它最终的样式, 称之为 Computed Style。

这一步完成后, 会得到一棵带有样式的 DOM 树。

带有样式的 DOM 树

可以通过 getComputedStyle() 获取元素的最终样式

布局 - Layout

布局完成后会得到布局树 Layout Tree, 有各种节点的几何信息、宽高尺寸以及位置

布局阶段会依次遍历 DOM 树的每一个节点, 计算每个节点的几何信息。例如节点的宽高、相对包含块的 位置

(但页面上的尺寸和位置是相互影响的, 一个元素的尺寸或位置变了, 可能会导致整个布局层的变化)
(这里的 位置 是什么位置?是相对谁的位置? —— 相对 包含块 的位置, 在其他md里呈现)
( 包含块 一个元素的活动区域, 像 相对定位/绝对定位 等)

大部分时候, DOM 树和布局树并非一一对应:

  • display:none 的节点没有几何信息, 因此不会生成到布局树

    <head>元素之所以被隐藏, 就是因为被添加了 display:none; 在这一步因为没有几何信息所以被忽略
    display:none

  • 使用了伪元素选择器, 虽然 DOM 树中不存在这些伪元素节点, 但它们 拥有 几何信息, 所以会生成到布局树中

伪元素

  • 还有匿名行盒、匿名块盒等等都会导致 DOM 树和布局树无法一一对应

    内容必须在行盒中, 行盒和块盒不能相邻
    匿名行盒、匿名块盒

Layout TreeC++ 对象, 不是DOM对象, 且布局树不能使用 JavaScript 进行操作, 但可以获取部分信息, 比如 document.body.clientWidth 等 明显不是 CSS 属性得到的
是否有几何信息, 可以在浏览器 Computed 查看, 若存在盒子 即表明存在几何信息

分层 - Layer

因为页面画出来之后, 并不是一成不变的, 是有可能会改变的
但每一次改变都需要全部重新渲染的话, 工作量太大 效率太低
因此浏览器(远古版本的没有)通过分层来提高效率。
页面在浏览过程中, 哪一个分层发生变化了, 浏览器就只需要重选渲染这一层即可
但存储分层的图层 会占用较大的内存空间, 所以会控制分层的数量
可以在 F12 - [更多工具More-Tools] - 图层Layers 查看

工作过程中, 渲染主线程 会使用一套复杂的策略对整个布局树中进行分层。

也可以通过改变部分 CSS 属性, 试图影响分层结果, 但最终如何分层, 还是由浏览器进行决策

部分 CSS 属性(与堆叠上下文有关的属性)可以影响分层结果(只是影响, 不一定是决定性的)

比如 z-index opacity transform 等样式都会或多或少的影响分层结果

还可以给通过给节点添加 will-change: transform; 告知浏览器 这个节点将来可能发生变化。更大程度的影响分层结果, 但还是由浏览器进行最终决策

滚动条也会单独分一个层

分层 - Layer

绘制 - Paint

这一步是为每一层 生成一系列的 绘制的指令

绘制的指令:

  • 将画笔移动到(0,0)处
  • 画一个200*300的矩形
  • 用红色填充矩形
  • ……

类似于canvas, 但canvas也是浏览器开放出来的功能, canvas调用的也是浏览器内核的功能

渲染主线程 会为每个层单独产生绘制指令集, 用于描述这一层的内容该如何画出来。

完成绘制后, 渲染主线程 将每个图层的绘制信息提交给 合成线程 , 剩余工作将由 合成线程 完成。

分块 - Tiling

合成线程(Compositor) 首先对每个图层进行分块, 将其划分为更多的小区域

分块

此过程会由 合成线程 启动的多个 分块线程(CompositorTileWorker) 完成

它会从 线程池 中拿取多个线程来完成分块工作。

合成线程 - 线程器

光栅化 - Raster

光栅化是将每个块变成位图, 位图就是每一个像素点的信息(如rgba等信息)
(这里的一个块含有多个像素点, 每个像素点含有不同的rgba等, 一个块用一个二维数组表示[[rgba][rgba][rgba]...])

光栅化 - 位图

合成线程 会将块信息交给 GPU 进程 , 以极高的速度完成光栅化。

GPU 进程 会在内部开启 多个线程 来完成光栅化, 并且优先处理 靠近视口区域(浏览器窗口正在看的位置) 的块。

光栅化完成之后, GPU进程 会把位图交回给 合成线程

光栅化的结果, 就是一块一块的位图

光栅化

画 - Draw

合成线程 拿到每个层、每个块的位图后, 生成一个个「**指引(quad)**」信息。

指引 会标识出每个位图应该画到屏幕的哪个位置, 以及会考虑到旋转、缩放等变形。

变形发生在 合成线程 , 与渲染主线程无关, 这就是 transform 效率高的本质原因
(transform是将位图 通过数学运算 矩阵运算得到)。

合成线程 会把 quad 提交给 GPU 进程, 由 GPU 进程 产生系统调用, 提交给 GPU 硬件, 完成最终的屏幕成像。

这里的 合成线程 之所以需要先把 quad 交给 GPU进程
是因为 合成线程 在浏览器的 渲染进程
渲染进程 是沙盒运行的, 无法直接调用操作系统命令
因此需要把工作委托给浏览器的 GPU进程
GPU进程再去调用物理层面的GPU进行加速处理和呈现

因为 渲染进程 是沙盒运行的, 因此现在的浏览器较为安全, 浏览网页并不会对 OS 层面造成影响

画 - Draw

什么是 Reflow

当增删一个节点的时候, 会导致 DOM树 发生改变
当修改一个元素节点的CSS属性(几何尺寸 几何信息 相关)时, 会导致 CSSOM树 发生改变

这些操作, 会导致浏览器重回 样式计算 - Recalculate Style 开始进行计算样式和渲染展示

这个步骤过程就是Reflow

reflow 的本质就是重新计算 layout 树, 是因为不管是 DOM树 还是 CSSOM树 (包括样式计算), 其实最终都是影响了layout树。

当进行了会影响布局树的操作后, 需要重新计算布局树, 会引发 layout。

为了避免连续的多次操作导致布局树反复计算, 浏览器会合并这些操作, 当 JS 代码全部完成后再进行统一计算。

所以, 改动属性造成的 reflow 是异步完成的。

也同样因为如此, 当 JS 获取布局属性时, 就可能造成无法获取到最新的布局信息。

浏览器在反复权衡下, 最终决定获取属性立即 reflow。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 渲染主线程

dom.style.width = xxx
dom.style.height = xxx
dom.style.padding = xxx
dom.style.margin = xxx

// 执行到这里的时候, 因为这段js代码还没有执行完, 所以上面的操作会先统一添加到 事件队列 里面等待渲染
// 而不是每改变一个属性值就立即reflow

// 但如果等到js代码完全执行完之后再进行reflow, 就会导致下面的console打印获取的是还没有渲染之前的值, 这是不正确的
// 因此浏览器规定, 在读取这些属性值的时候, 主线程会先立即把上面的更改渲染拉下来 当作同步队列执行, reflow完成之后再读取这个值
// 这样就能保证读取到的值是正确的
console.log(dom.clientWidth)

什么是 Repaint

重绘。就是回到 绘制 - Paint 步骤, 如改变了颜色等。

repaint 的本质就是重新根据 分层信息 计算了绘制指令。

当改动了 可见样式 后, 就需要重新计算, 会引发 repaint。

由于元素的 布局信息 也属于 可见样式 , 所以 reflow 一定会引起 repaint。

为什么 Transform 效率高

因为是通过计算改变位图来实现的, 并不会改变布局。

Transform 是直接回到 画 - Draw 的步骤, 并不需要经历前面的步骤

如果是 js 改变 transform , 那会导致 CSSOM树 改变

但如果使用 animation 的话, CSSOM 都没变, 效率会极大地提高