浏览器到底是怎么跑起来的?从单进程到多进程,彻底搞懂 Chrome 底层架构
大家好,今天我们来直接扒开浏览器的底层,看看它到底是怎么把一堆 HTML、CSS、JavaScript 变成我们眼前这个炫酷网页的。
看完后你会明白:
- 为什么 Chrome 那么丝滑却吃内存?
- 为什么一个标签页卡死不会影响其他标签?
- 为什么面试总爱问「浏览器多进程架构」?
一、打开 Chrome,到底发生了什么?
你双击 Chrome 图标那一瞬间,操作系统干了这么几件事:
- 启动一个「主进程」(Browser Process),它有唯一的 PID(进程 ID)
- 这个主进程就是整个浏览器的「大脑」,负责:
- 绘制浏览器界面(地址栏、书签栏、前进后退按钮)
- 处理用户交互(点击、输入、滚轮)
- 管理所有子进程的「生杀大权」
- 提供存储功能(Cookie、LocalStorage 等)
记住:进程是操作系统分配资源的最小单位,线程才是真正干活的(执行代码的最小单位)。
二、曾经的「单进程浏览器」:IE 的血泪史
还记得 IE6、IE8 吗?那是一个悲伤的故事。
早期浏览器(包括老版 IE、Firefox)都是单进程架构(对应图1):
主线程(唯一)
├── 渲染 HTML/CSS/JS
├── 执行 JavaScript(V8 前身)
├── 处理用户交互
├── 网络请求
├── 插件(Flash!)
└── 本地存储
所有事情都在一个进程、一个主线程里干!
后果是什么?
- 一个页面卡死 → 整个浏览器卡死
- 一个页面崩溃 → 整个浏览器崩溃(经典的「IE 已停止工作」)
- 一个恶意页面 → 可以轻松搞垮你整个浏览器
所以当年网吧里一排电脑,可能会同时弹出十几条「IE 已停止工作」的弹窗。
三、Chrome 的革命:多进程架构
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 线程 | 处理网络、文件读写 |
所以你写 setTimeout、Promise、async/await 时,实际上是主线程把任务扔给了浏览器其他模块处理,然后通过回调放回事件队列。这就是「异步」的底层原理。
我们来深度解析一下
执行顺序(永远不会错的 6 步循环):
- JS 主线程开始执行一个宏任务(比如整个 js代码)
- 遇到同步代码 → 直接执行
- 遇到异步操作 → 立刻“注册”给对应模块(定时器线程 / 网络进程 / V8 微任务)
- 当前宏任务的同步代码全部执行完
- Event Loop 执行清空微任务队列(所有微任务一次性执行完!)
- 浏览器更新渲染(Layout → Paint → Composite)
- Event Loop 从宏任务队列取下一个宏任务 → 回到第 1 步
有点抽象,Event Loop 是啥?
Event Loop 的真实身份(官方名称 + 位置)
- 真实名字:在 Chromium 源码里叫 MessageLoop / TaskScheduler(Blink 引擎实现)
- 所属位置:每个渲染进程内部(每个标签页一个 Event Loop!)
- 它本身不是线程,只是一个死循环(while true)
为什么说他是死循环呢? 它一辈子只干一件事(就这一件事,但超级重要):
不停地盯着两个地方:
- JS 主线程,你现在忙完了吗?
- 任务队列(宏任务 + 微任务),有没有新任务来了?
只要两个条件同时满足(主线程空闲 + 队列里有任务),它就立刻把队列里的任务塞给 JS 主线程执行。
那么他干了什么呢?
用生活比喻(100% 能记住)
想象 JS 主线程是一个超级大厨(只能同时干一件事)
而且JS主线程还是一个啥活都干被经理疯狂压榨的大厨
Event Loop 就是餐厅经理,它每时每刻在做一件事:
- 看大厨有没有忙完(主线程空闲?)
- 看小票机(任务队列)有没有新单子
- 只要大厨闲着 + 有单子 → 立刻把单子塞给大厨:“下一单!炒这个!”
- 大厨炒完一道菜(宏任务结束)→ 经理立刻把所有“洗碗、擦桌子”(微任务)全部扔给大厨让他干完
- 再喊服务员上菜(渲染)
- 然后再取下一张大单子
终极一句话总结(背下来,面试直接甩)
Event Loop 就是渲染进程里一个永不停止的死循环,唯一职责是: 当 JS 主线程空闲时,依次从宏任务队列取一个任务执行 → 执行完后立刻清空所有微任务 → 允许浏览器渲染 → 再取下一个宏任务…… 周而复始。
它不干活,它只“派活”!
它不执行代码,它只决定“什么时候执行哪段代码”!
六、从输入 URL 到页面呈现,到底经历了什么?
我们来完整走一遍(结合图3):
- 你在地址栏输入
https://juejin.cn - 浏览器主进程 → 网络进程 → 发起 HTTP 请求
- 网络进程拿到 HTML → 交给某个渲染进程
- 渲染进程开始解析:
- 主线程:解析 HTML → 构建 DOM 树
- 主线程:解析 CSS → 构建 CSSOM 树
- 主线程:合并成 Render Tree
- 主线程:Layout(布局)
- 主线程:Paint(绘制绘制列表)
- 把绘制工作交给合成线程 → 分图层 → 光栅化 → 交给 GPU 进程
- 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 进程全局只有一个?
- 显卡只有一块!操作系统只允许一个进程直接跟显卡硬件对话(防止冲突)
- GPU 进程负责:
- 接收所有渲染进程发来的「图层 + 合成指令」
- 做最终的图层合成(Compositing)
- 把最终画面甩给显卡显示
- 如果每个标签页一个 GPU 进程 → 100 个标签页就要 100 个 GPU 进程 → 显卡直接爆炸
真实验证方法(你现在就可以打开 Chrome 试试):
- 打开 Chrome → Shift + Esc → Chrome 自带任务管理器
- 往下拉,你会看到只有一行写着「GPU 进程」,不管你开了多少标签页,永远只有这一个!
八、最后:为什么 Chrome 能称霸浏览器市场?
因为它在 2008 年就干了三件「超前」的事:
- 多进程架构 → 稳定、安全
- V8 引擎 → JavaScript 快到飞起
- 硬件加速 + GPU 进程 → 网页终于能做动画了!
这三板斧直接把 IE、Firefox 按在地上摩擦,至今无人能敌。
最后
当你下次再看到任务管理器里几十个 chrome.exe 进程时,不要骂「Chrome 真吃内存」,而应该感慨:
「这货为了不让我一个页面崩全崩,拼了老命了啊!」
这就是多进程架构的代价,也是我们能安心刷网页、写前端的根本原因。