浏览器到底是怎么跑起来的?从单进程到多进程,彻底搞懂 Chrome 底层架构

125 阅读11分钟

浏览器到底是怎么跑起来的?从单进程到多进程,彻底搞懂 Chrome 底层架构

大家好,今天我们来直接扒开浏览器的底层,看看它到底是怎么把一堆 HTML、CSS、JavaScript 变成我们眼前这个炫酷网页的。

看完后你会明白:

  • 为什么 Chrome 那么丝滑却吃内存?
  • 为什么一个标签页卡死不会影响其他标签?
  • 为什么面试总爱问「浏览器多进程架构」?

一、打开 Chrome,到底发生了什么?

你双击 Chrome 图标那一瞬间,操作系统干了这么几件事:

  1. 启动一个「主进程」(Browser Process),它有唯一的 PID(进程 ID)
  2. 这个主进程就是整个浏览器的「大脑」,负责:
    • 绘制浏览器界面(地址栏、书签栏、前进后退按钮)
    • 处理用户交互(点击、输入、滚轮)
    • 管理所有子进程的「生杀大权」
    • 提供存储功能(Cookie、LocalStorage 等)

记住:进程是操作系统分配资源的最小单位,线程才是真正干活的(执行代码的最小单位)。

二、曾经的「单进程浏览器」:IE 的血泪史

71fd5a00908f5af62b47133492f93d63.png

还记得 IE6、IE8 吗?那是一个悲伤的故事。

早期浏览器(包括老版 IE、Firefox)都是单进程架构(对应图1):

主线程(唯一)
├── 渲染 HTML/CSS/JS
├── 执行 JavaScript(V8 前身)
├── 处理用户交互
├── 网络请求
├── 插件(Flash!)
└── 本地存储

所有事情都在一个进程、一个主线程里干!

后果是什么?

  • 一个页面卡死 → 整个浏览器卡死
  • 一个页面崩溃 → 整个浏览器崩溃(经典的「IE 已停止工作」)
  • 一个恶意页面 → 可以轻松搞垮你整个浏览器

所以当年网吧里一排电脑,可能会同时弹出十几条「IE 已停止工作」的弹窗。

三、Chrome 的革命:多进程架构

4ec2e4ab5b33140a856bee55f882d7f0.png

cf4802e488c3047c587458721bdb25d4.png Google 在 2008 年推出 Chrome,直接把浏览器架构干翻重做,搞出了今天我们熟知的「多进程 + 多线程」架构。

我们来一张最经典的 Chrome 进程架构图:

浏览器主进程(Browser Process)← 唯一,管全局
├── GPU 进程(最多1个)           → 3D 绘制、硬件加速
├── 网络进程(1个)               → 所有网络请求
├── 插件/扩展进程(多个)          → 每个扩展独立进程(安全)
└── 数个站点渲染进程(Renderer Process) ← 重头戏!
     每个标签页/iframe(不同站点)一个渲染进程

每个渲染进程内部又是什么结构?

渲染进程(Renderer Process)
├── Blink(排版引擎,前身 WebKit)
├── V8(JavaScript 引擎)
├── 合成线程(Compositor Thread)
├── 光栅化线程(Raster Thread)
└── 其他辅助线程

注意:JavaScript 主线程是单线程的!这是 V8 的设计哲学,也是为什么我们总说「JS 是单线程语言」。

总结构就是这样:

全局(整个浏览器只有一个)
├─ 浏览器主进程(Browser Process)
├─ 网络进程(Network Service)        ← 所有标签页共用!
├─ GPU 进程                          ← 所有标签页共用!
└─ 插件/扩展进程(每个插件一个)

每个标签页(默认情况)
└─ 1 个独立的渲染进程(Renderer Process)
   ├─ JS 主线程(唯一能执行 JS 的线程)
   ├─ Event Loop(事件循环)
   ├─ 微任务队列(Promise.then 等)
   ├─ 宏任务队列(setTimeout、fetch.then 等)
   ├─ 定时器线程(管理 setTimeout/setInterval)
   ├─ 合成线程(处理 transform、scroll)
   ├─ 光栅化线程池
   ├─ 文件/数据库线程
   └─ Web Worker 线程(可选)

四、为什么每个标签页都要独立进程?值吗?

值!太值了!

项目单进程浏览器Chrome 多进程
稳定性一个标签崩,全崩一个标签崩,其他无影响
安全性同源策略容易被绕进程隔离 + 站点隔离(Site Isolation)
性能资源抢占严重资源独立分配
内存占用高(但现代电脑扛得住)

Chrome 甚至更狠:从 Chrome 67 开始,默认开启 Site Isolation(站点隔离),即使是同一个域下的不同子域(a.example.com 和 b.example.com),也会强制用不同渲染进程!

这就是为什么你打开十几个标签,任务管理器里会有 20+ 个 chrome.exe 进程。

五、那异步任务、宏任务微任务、Event Loop 呢?

很多人把「JavaScript 是单线程」和「浏览器是多进程」搞混了。

澄清一下:

  • JavaScript 引擎(V8)是单线程的 → 所以有 Event Loop、宏任务、微任务
  • 但浏览器不是单线程的!

一个渲染进程里,其实有这些线程在默默干活:

线程名称职责
主线程(JS 线程)执行 JS、解析 HTML/CSS、布局、绘制
合成线程处理 transform、opacity(不触发重排重绘)
光栅化线程把图层变成位图(交给 GPU)
I/O 线程处理网络、文件读写

所以你写 setTimeoutPromiseasync/await 时,实际上是主线程把任务扔给了浏览器其他模块处理,然后通过回调放回事件队列。这就是「异步」的底层原理。

我们来深度解析一下

执行顺序(永远不会错的 6 步循环):

  1. JS 主线程开始执行一个宏任务(比如整个 js代码)
  2. 遇到同步代码 → 直接执行
  3. 遇到异步操作 → 立刻“注册”给对应模块(定时器线程 / 网络进程 / V8 微任务)
  4. 当前宏任务的同步代码全部执行完
  5. Event Loop 执行清空微任务队列(所有微任务一次性执行完!)
  6. 浏览器更新渲染(Layout → Paint → Composite)
  7. Event Loop 从宏任务队列取下一个宏任务 → 回到第 1 步

有点抽象,Event Loop 是啥?

a8a0956310fbc3378caa4831a0918c2b.jpg

Event Loop 的真实身份(官方名称 + 位置)

  • 真实名字:在 Chromium 源码里叫 MessageLoop / TaskScheduler(Blink 引擎实现)
  • 所属位置:每个渲染进程内部(每个标签页一个 Event Loop!)
  • 它本身不是线程,只是一个死循环(while true)

为什么说他是死循环呢? 它一辈子只干一件事(就这一件事,但超级重要):

不停地盯着两个地方:

  1. JS 主线程,你现在忙完了吗?
  2. 任务队列(宏任务 + 微任务),有没有新任务来了?

只要两个条件同时满足(主线程空闲 + 队列里有任务),它就立刻把队列里的任务塞给 JS 主线程执行。

那么他干了什么呢?

用生活比喻(100% 能记住)

89d53a4ca51edaac0fbc01349a38c535.jpg 想象 JS 主线程是一个超级大厨(只能同时干一件事) 而且JS主线程还是一个啥活都干被经理疯狂压榨的大厨 Event Loop 就是餐厅经理,它每时每刻在做一件事:

  1. 看大厨有没有忙完(主线程空闲?)
  2. 看小票机(任务队列)有没有新单子
  3. 只要大厨闲着 + 有单子 → 立刻把单子塞给大厨:“下一单!炒这个!”
  4. 大厨炒完一道菜(宏任务结束)→ 经理立刻把所有“洗碗、擦桌子”(微任务)全部扔给大厨让他干完
  5. 再喊服务员上菜(渲染)
  6. 然后再取下一张大单子

终极一句话总结(背下来,面试直接甩)

Event Loop 就是渲染进程里一个永不停止的死循环,唯一职责是: 当 JS 主线程空闲时,依次从宏任务队列取一个任务执行 → 执行完后立刻清空所有微任务 → 允许浏览器渲染 → 再取下一个宏任务…… 周而复始。

它不干活,它只“派活”!

它不执行代码,它只决定“什么时候执行哪段代码”!

六、从输入 URL 到页面呈现,到底经历了什么?

我们来完整走一遍(结合图3):

  1. 你在地址栏输入 https://juejin.cn
  2. 浏览器主进程 → 网络进程 → 发起 HTTP 请求
  3. 网络进程拿到 HTML → 交给某个渲染进程
  4. 渲染进程开始解析:
    • 主线程:解析 HTML → 构建 DOM 树
    • 主线程:解析 CSS → 构建 CSSOM 树
    • 主线程:合并成 Render Tree
    • 主线程:Layout(布局)
    • 主线程:Paint(绘制绘制列表)
  5. 把绘制工作交给合成线程 → 分图层 → 光栅化 → 交给 GPU 进程
  6. GPU 进程把画面甩给显卡 → 显示器刷新 → 你看到页面!

其中第 4 步的「主线程」就是我们常说的「JavaScript 主线程」,一旦它被长任务阻塞(比如死循环),页面就卡了。

七、常见的误区和面试雷区提醒

1. 错的:「浏览器是多线程的」

正确:「浏览器是多进程的,每个渲染进程内部有很多线程,但 JavaScript 主线程是单线程的」

为什么很多人说“浏览器是多线程的”?
因为打开 Chrome 任务管理器,一堆线程!所以直觉觉得“浏览器是多线程的”。

真相是:

  • 浏览器整体是多进程(Browser 进程 + 多个渲染进程 + 网络进程 + GPU 进程……)
  • 每个渲染进程内部确实是多线程(JS 主线程 + 合成线程 + 光栅化线程 + 定时器线程 + 文件线程……)
  • 但最关键的 JS 主线程永远只有一个、永远单线程!
  • 所以你写的所有 JS 代码(包括所有 async/await、Promise、setTimeout 的回调)永远只会在这根单线程上排队执行。

一句话记住:
浏览器是“多进程 + 每进程多线程”,但对我们前端来说,命门永远是那一根「JS 主线程」。

2. 错的:「打开一个页面就一个进程」

正确:「一个页面可能有多个渲染进程」

默认情况:一个标签页 = 一个渲染进程
真实情况(2025 年 Chrome 默认开启的功能):

  • Site Isolation(站点隔离):即使同站点的 a.com 和 b.a.com 也会强制用不同渲染进程
  • 跨源 iframe:每个不同源的 iframe 都会独立一个渲染进程
  • OOPIF(Out-of-Process iframe):现在几乎所有跨站 iframe 都会单独进程

结果:你打开一个普通页面(比如掘金文章页),任务管理器里可能看到 5~10 个渲染进程都属于这一个标签页!

3. 错的:「async/await 是多线程」

正确:「还是单线程,只是语法糖,底层还是事件循环」

很多人被骗的原因
async/await 写起来像同步代码,执行效果又像多线程,所以误以为它开了线程。

真相

async function foo() {
  console.log('1');
  await fetch('xxx');
  console.log('2');  // 这句其实被编译器自动变成了 .then 回调
}

await 后面的代码会被自动包装成一个 Promise.then(微任务),扔到微任务队列,仍然在同一根 JS 主线程执行!

永远记住:不管你写得多像同步,JS 永远只有一根线程在跑。

4. 错的:「GPU 进程是每个标签页一个」

正确:「GPU 进程全局只有一个」(你重点问的点)

这点最最最容易错!我们来画一张图彻底搞懂:

整个 Chrome 浏览器(启动后就一直存在)
│
├─ Browser 主进程(1个)
├─ 网络进程(1个)                ← 所有标签页共用
├─ GPU 进程(1个) ←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←
│     ↑
│     ↑  全局只有一个!不管你开 1 个标签还是 100 个标签
│     ↑  所有标签页、所有渲染进程的画面最终都送到这一个 GPU 进程
│     ↑  再由它统一交给显卡绘制
│
├─ 插件进程(每个插件1个)
└─ 无数个渲染进程(每个标签页/iframe 可能多个)

为什么 GPU 进程全局只有一个?

  1. 显卡只有一块!操作系统只允许一个进程直接跟显卡硬件对话(防止冲突)
  2. GPU 进程负责:
    • 接收所有渲染进程发来的「图层 + 合成指令」
    • 做最终的图层合成(Compositing)
    • 把最终画面甩给显卡显示
  3. 如果每个标签页一个 GPU 进程 → 100 个标签页就要 100 个 GPU 进程 → 显卡直接爆炸

真实验证方法(你现在就可以打开 Chrome 试试):

  1. 打开 Chrome → Shift + Esc → Chrome 自带任务管理器
  2. 往下拉,你会看到只有一行写着「GPU 进程」,不管你开了多少标签页,永远只有这一个!

八、最后:为什么 Chrome 能称霸浏览器市场?

因为它在 2008 年就干了三件「超前」的事:

  1. 多进程架构 → 稳定、安全
  2. V8 引擎 → JavaScript 快到飞起
  3. 硬件加速 + GPU 进程 → 网页终于能做动画了!

这三板斧直接把 IE、Firefox 按在地上摩擦,至今无人能敌。

最后

当你下次再看到任务管理器里几十个 chrome.exe 进程时,不要骂「Chrome 真吃内存」,而应该感慨:

「这货为了不让我一个页面崩全崩,拼了老命了啊!」

这就是多进程架构的代价,也是我们能安心刷网页、写前端的根本原因。