# Chrome架构:仅仅打开了1个页面,为什么有4个进程
30 秒速记
- 页面 ≠ 进程:页面是你看到的内容,进程是操作系统分资源、做隔离的单位
- 文中那 4 个 = 浏览器主进程 +
GPU进程 + 网络进程 + 渲染进程 - 拆开的目的:一个页面崩了不拖垮整个浏览器,渲染进程还能关进沙箱
- 现在的
Chrome往往不止 4 个:Storage、Audio等Utility进程、扩展进程、预启动的备用渲染进程,开了站点隔离后跨站iframe也会单独开进程 - 看归属用
Chrome自带任务管理器(Shift + Esc),比系统任务管理器好对应
打开一个页面就有好几个进程,是因为 Chrome 按职责拆分了进程,页面和进程本来就不是一一对应的。 文中截图里的 4 个分别是浏览器主进程、GPU 进程、网络进程和渲染进程:主进程管界面和调度,网络进程负责下载资源,渲染进程跑 Blink 和 V8 把页面画出来,GPU 进程负责最终合成上屏。这么拆是为了隔离,页面崩了只死一个渲染进程。不过新版本 Chrome 一般比 4 个多,还会有存储、音频这类服务进程和扩展进程,具体数量别背死,打开 Shift + Esc 看一眼就知道。
在开始之前,我们一起看下,Chrome打开一个页面需要启动多少进程?你可以点击Chrome浏览器右上角的“选项”菜单,选择“更多工具”子菜单,点击“任务管理器”,这将打开Chrome的任务管理器的窗口,如下图

和Windows任务管理器一样,Chrome任务管理器也是用来展示运行中Chrome使用的进程信息的。从图中可以看到,Chrome启动了4个进程,你也许会好奇,只是打开了1个页面,为什么要启动这么多进程呢?
在解答这个问题之前,我们需要了解一下进程的概念,不过由于好多人容易把进程和线程的概念混淆,从而影响后续其他概念的理解,所以这里我就将这两个概念以及它们之间的关系一并为你讲解下。
原理拆解: 页面是用户看到的内容单位,进程是操作系统分配地址空间、调度资源和建立故障边界的运行单位,两者不存在一一对应关系。Chrome 会把浏览器控制逻辑、页面渲染以及部分高风险或专用能力拆到不同进程中,使某个页面或组件异常时不必让整个浏览器同时退出,也便于实施权限隔离。因此,打开一个标签页产生多个进程,是按职责分工后的结果,而不是重复运行了多个页面。
观察方式: Chrome 自带任务管理器比系统任务管理器更容易把进程与标签页、扩展或浏览器组件对应起来。可以记录空白启动时的条目,再打开普通页面、跨站页面或启用扩展,比较条目数量和资源占用变化;系统任务管理器则可辅助核对进程标识符。观察到的“四个”只能说明题目截图对应环境中的一次运行状态,不能据此写成所有版本固定存在四个进程。
边界与排查: 实际构成会受 Chrome 版本、操作系统、站点隔离策略、扩展、页面内容、后台服务以及进程复用策略影响。同一站点的页面可能复用某些资源,不同站点或特殊能力也可能触发额外进程。验证时应固定浏览器版本与启动参数,使用无痕窗口或临时用户配置减少扩展干扰,并以 Chrome 任务管理器的实时条目为准;若进程数与预期不同,应先检查扩展、后台页面和站点隔离,而不是直接判定浏览器异常。
面试官追问
追问 1只开了一个商品页,系统任务管理器里却有七八个 Chrome 进程,是不是页面被重复打开了?
不是。主进程、GPU、网络、渲染这几个是标配,新版本还会拆出存储、音频等服务进程,装了扩展每个扩展也可能占一个。按 Shift + Esc 打开 Chrome 任务管理器,每一行都写着是标签页、扩展还是什么服务。
追问 2想确认多出来的进程是扩展带来的,怎么验证最快?
开一个无痕窗口(默认不加载扩展)或者用 --user-data-dir 起一个干净的配置,打开同一个页面对比任务管理器里的条目。多出来的那几行基本就是扩展进程。
追问 3测试用例里写死「打开一个页面必须是 4 个进程」,合理吗?
不合理。进程数随 Chrome 版本、操作系统、扩展、站点隔离策略、内存情况都会变,Android 上为了省内存网络服务还可能跑在主进程里。要验证隔离就验证行为,比如让一个页面死循环,看其他标签页还能不能操作。
追问 4一个页面里嵌了三个不同站点的 iframe,进程会怎么分?
桌面 Chrome 从 67 开始默认开站点隔离,跨站的 iframe 会放进各自的渲染进程,所以这个页面可能对应 4 个渲染进程。这样做是为了防 Spectre 这类侧信道攻击,代价是内存更高。
追问 5页面卡死了,但别的标签页还能用,先从哪看起?
Shift + Esc 找到那个标签页对应的进程,看 CPU 是不是一直打满,打满大概率是 JS 死循环或者长任务,可以直接在这里结束这个进程。不用杀整个浏览器,其他页面在别的渲染进程里,不受影响。
# 进程和线程
30 秒速记
- 进程 = 程序跑起来的一个实例,有自己独立的内存空间;线程 = 进程里真正干活的执行单元,不能单独存在
- 同一进程的线程共享内存 → 通信便宜,但要加锁 / 原子操作,否则有竞态
- 线程共享故障域:一个线程把进程搞崩(内存越界、未处理的致命错误),其他线程一起没
- 进程之间内存隔离 → 一个崩了不影响别的,代价是交换数据要走
IPC,有序列化开销 - 进程退出时操作系统回收它所有资源,包括线程泄漏的内存
进程是操作系统分资源的单位,线程是进程里被调度执行的单位,打个比方,进程是一个工厂,线程是工厂里的工人。 同一个工厂的工人共用仓库,拿东西很快,但两个人同时改一个数就会出错,所以要同步;一个工人放火把厂烧了,所有人都得停。不同工厂互相看不到对方仓库,要传东西只能走 IPC 寄快递,慢但安全。浏览器拆成多进程,本质就是用通信成本换隔离。
不过,在介绍进程和线程之前,我需要先讲解下什么是并行处理,因为如果你理解了并行处理的概念,那么再理解进程和线程之间的关系就会变得轻松许多。
原理拆解: 进程提供独立地址空间和资源容器,线程则是其中被调度执行的单元;一个进程至少要有一个线程,线程不能脱离所属进程存在。同一进程中的线程能够直接访问共享内存,这让任务拆分和结果交换成本较低,但也要求同步控制,否则会出现竞态。进程之间默认不能直接读取彼此内存,交换数据通常要经过管道、套接字或其他 IPC 机制,因此隔离更强、通信成本也更高。
最小验证: 输入为两个工作线程,各自把同一计数器累加 10000 次;要验证的结论是共享数据必须同步,并且主线程可以等待两个执行单元完成。将代码保存为 threads.js,使用 Node.js 运行。
const { Worker, isMainThread, workerData, parentPort } = require('node:worker_threads');
if (isMainThread) {
const shared = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT);
const counter = new Int32Array(shared);
const jobs = [1, 2].map(() => new Promise((resolve, reject) => {
const worker = new Worker(__filename, { workerData: shared });
worker.once('message', resolve);
worker.once('error', reject);
}));
Promise.all(jobs).then(() => {
console.log(Atomics.load(counter, 0));
});
} else {
const counter = new Int32Array(workerData);
for (let i = 0; i < 10000; i++) {
Atomics.add(counter, 0, 1);
}
parentPort.postMessage('done');
}
关键行 SharedArrayBuffer 建立显式共享区域,Atomics.add 使每次累加不可被并发覆盖,预期输出为 20000。若改成普通的“读取、加一、写回”,结果可能因竞态小于预期;若运行环境禁用工作线程或不支持相关 API,程序会直接报错。
边界与排查: “某线程出错会拖垮进程”描述的是共享故障域,不代表每个业务异常都必然导致进程退出;例如 Node.js 工作线程的异常可通过 error 事件被宿主管理,浏览器也会增加额外隔离层。相反,原生崩溃、内存破坏或未处理的致命错误仍可能终止整个进程。排查并发问题时应区分数据竞态、线程异常和进程崩溃,并结合进程标识、线程信息、崩溃日志及退出码判断故障发生在哪一层。
面试官追问
追问 1两个 Worker 线程各给共享计数器加 10000 次,结果偶尔小于 20000,为什么?
count++ 是「读、加一、写回」三步,两个线程交错执行就会互相覆盖。改成 Atomics.add(counter, 0, 1),计数器放在 SharedArrayBuffer 上,这一步就是原子的,结果稳定 20000。
追问 2某个工作线程抛了异常,整个 Node.js 进程就一定会挂吗?
不一定。worker_threads 里未捕获的异常会以 error
