浏览器多进程架构
一、进程、线程、协程基础
进程是什么
进程是操作系统分配资源的基本单位。
可以简单理解为:
一个正在运行的程序 = 一个或多个进程比如打开 Chrome 时,操作系统会为 Chrome 创建多个进程。
每个进程通常拥有自己独立的:
内存空间
文件句柄
网络连接
权限上下文
运行状态
进程 ID
进程之间默认是相互隔离的:
进程 A 的内存
进程 B 不能随便访问
进程 B 崩溃
一般不会直接把进程 A 搞崩因此,进程隔离带来的好处包括:
稳定性更好
安全性更好
一个进程崩溃不容易影响其他进程
可以利用多核 CPU 并行运行
但是进程也比较“重”,因为每个进程都有独立资源,所以:
创建成本高
销毁成本高
进程间通信成本高
占用内存更多
线程是什么
线程是 CPU 调度执行的基本单位。
一个进程里面可以有多个线程:
进程
├─ 线程 A
├─ 线程 B
└─ 线程 C线程共享所在进程的资源,比如:
共享内存空间
共享文件句柄
共享全局变量
共享部分运行环境
线程比进程更轻量,多个线程可以在同一个进程里并发执行。
比如一个渲染进程里可以有:
主线程
合成线程
光栅化线程
Worker 线程
线程的优点是:
创建和切换成本比进程低
通信更方便,因为共享内存
可以更细粒度地并发执行任务
线程的缺点是:
容易出现竞态条件
容易出现死锁
可能出现数据不一致
可能出现锁竞争
需要关注线程安全
例如:
线程 A 正在修改变量 count
线程 B 也在修改变量 count
如果没有同步控制,就可能出现数据错乱协程是什么
协程是一种更轻量的用户态执行单元。
它不是操作系统直接调度的,而是由程序自己控制切换。
可以这样理解:
进程:资源容器
线程:CPU 调度单位
协程:程序内部的轻量级任务切换机制线程切换通常由操作系统调度,协程切换通常由程序主动让出执行权。
线程:
由操作系统决定什么时候切换
协程:
由程序在某些点主动暂停、恢复前端里的 async / await 是协程吗
严格来说,JavaScript 里的 async / await 更接近一种异步控制流语法,它会把后续逻辑拆成 Promise 微任务继续执行。
例如:
async function main() {
console.log(1)
await fetch('/api/user')
console.log(2)
}
main()
console.log(3)大致执行过程是:
console.log(1)
遇到 await,当前 async 函数暂停
继续执行外层同步代码 console.log(3)
fetch 完成后,把 await 后面的逻辑放入微任务队列
最后执行 console.log(2)它不会创建一个新的操作系统线程。
所以:
async / await 不是多线程
async / await 不会让 JS 并行执行
async / await 只是让异步流程写起来像同步代码进程、线程、协程对比
对比项 | 进程 | 线程 | 协程 |
|---|---|---|---|
调度者 | 操作系统 | 操作系统 | 程序自身 / 运行时 |
资源隔离 | 强 | 弱,共享进程资源 | 更弱,依赖语言运行时 |
创建成本 | 高 | 中 | 低 |
切换成本 | 高 | 中 | 低 |
是否共享内存 | 默认不共享 | 共享 | 通常共享 |
是否能利用多核并行 | 可以 | 可以 | 单线程协程本身不一定可以 |
典型场景 | 浏览器多进程、服务进程 | 渲染线程、Worker 线程 | async/await、生成器、异步任务调度 |
二、浏览器为什么要用多进程架构
现代浏览器,尤其是 Chrome,采用的是多进程架构。
核心原因有四个:
稳定性
安全性
性能
隔离性稳定性
一个页面崩溃不应该拖垮整个浏览器。
如果所有页面都运行在一个进程里:
Tab A 崩溃
↓
整个浏览器可能崩溃
↓
Tab B、Tab C、下载任务、浏览器窗口全都受影响多进程后:
Tab A 的渲染进程崩溃
↓
通常只影响 Tab A
↓
浏览器主进程还在
↓
其他 Tab 仍然可以继续运行这就是为什么 Chrome 某个页面崩溃时,通常只看到这个 Tab 显示崩溃页面,而不是整个浏览器退出。
安全性
浏览器要运行来自不同网站的代码。
比如你同时打开:
bank.com
evil.com
github.com
mail.google.com如果它们都在同一个进程里,共享同一片内存空间,风险会非常高。
多进程隔离可以让不同站点运行在不同的沙箱环境中。
即使某个页面存在漏洞,攻击者也更难直接影响其他站点的数据。
性能
现代 CPU 通常是多核的。
多进程架构可以让不同页面、不同任务并行执行:
进程 A 在 CPU 核心 1 上执行
进程 B 在 CPU 核心 2 上执行
进程 C 在 CPU 核心 3 上执行
GPU 进程处理图形任务
Network 进程处理网络任务如果浏览器所有任务都挤在一个主线程里,页面会非常容易卡顿。
隔离性
浏览器不是只负责执行 JS。
它还要负责:
地址栏
标签页管理
网络请求
HTML 解析
CSS 解析
JS 执行
页面布局
页面绘制
GPU 合成
文件访问
扩展插件
安全沙箱
存储管理
这些职责拆分到不同进程后,架构更清晰,也更容易做权限控制和崩溃隔离。
三、现代浏览器的核心进程
浏览器大致可以拆成这些进程:
Browser Process
Network Process
Renderer Process
GPU Process
Utility Process
Extension Process不同浏览器实现细节不完全一样,但核心思想类似。
Browser Process:浏览器主进程
Browser Process 是浏览器的大脑。
它不直接负责解析 HTML、执行 JS、绘制页面,而是负责浏览器整体管理和调度。
标签页和窗口管理
例如:
新建标签页
关闭标签页
切换标签页
拖动窗口
最大化 / 最小化
地址栏输入
书签栏
下载栏
这些属于浏览器外壳层能力,不属于网页本身。
导航管理
当你在地址栏输入一个 URL:
https://example.com首先接收到这个操作的是 Browser Process。
它会决定:
这是搜索词还是 URL?
当前页面是否允许跳转?
是否需要新建一个渲染进程?
是否命中浏览器缓存?
是否需要让 Network Process 发请求?
响应回来后交给哪个 Renderer Process 处理?
进程管理
Browser Process 会管理其他子进程。
Browser Process
├─ Renderer Process A
├─ Renderer Process B
├─ Network Process
├─ GPU Process
└─ Utility Process如果某个 Renderer Process 崩溃,Browser Process 可以感知到,并在对应 Tab 上显示崩溃提示。
权限管理
网页想访问摄像头、麦克风、地理位置时,通常不会自己直接拿权限。
流程一般是:
网页请求权限
↓
Renderer Process 把请求交给 Browser Process
↓
Browser Process 弹出权限确认
↓
用户允许或拒绝
↓
结果返回给页面这样可以避免网页绕过浏览器直接访问敏感系统资源。
Renderer Process:渲染进程
Renderer Process 是前端最需要重点理解的进程。
它负责运行网页内容。
一个网页里的大部分前端逻辑,都发生在 Renderer Process 中。
Renderer Process 主要职责
包括:
解析 HTML
构建 DOM Tree
解析 CSS
构建 CSSOM Tree
执行 JavaScript
处理 DOM 操作
计算样式
Layout 布局
Paint 绘制
生成图层
处理用户事件
运行 Web Worker
与 Browser Process 通信
与 GPU Process 协作完成最终显示
可以简单理解为:
Renderer Process = 负责把网页跑起来的进程Renderer Process 内部的重要线程
渲染进程不是只有一个线程,它内部通常有多个线程协作:
Renderer Process
├─ Main Thread
├─ Compositor Thread
├─ Raster Worker Threads
├─ Worker Threads
└─ 其他辅助线程Main Thread:渲染主线程
主线程是最重要的线程。
前端经常说的:
JS 是单线程的主要指的是:
同一个页面中,可以操作 DOM 的 JS 主要运行在 Renderer Process 的 Main Thread 上主线程负责很多关键工作:
执行 JavaScript
解析 HTML
构建 DOM
解析 CSS
计算样式
执行 Layout
处理部分用户事件
执行 requestAnimationFrame 回调
触发微任务
处理 DOM 修改
所以主线程非常忙。
如果写了一个很耗时的 JS 任务:
while (true) {
// 无限循环
}页面就会卡死。
原因是:
主线程被 JS 占满
↓
不能响应点击
↓
不能处理输入
↓
不能更新 DOM
↓
不能进行布局和渲染
↓
页面卡死Compositor Thread:合成线程
合成线程主要负责页面图层的合成。
浏览器渲染页面时,不一定每次都重新执行完整的:
Style
Layout
Paint
Composite有些变化可以只走合成阶段。
比如:
.box {
transform: translateX(100px);
opacity: 0.5;
}这类属性往往可以交给合成线程处理,不一定需要重新布局。
所以动画优化中经常说:
优先使用 transform 和 opacity原因就在这里。
Raster Worker Threads:光栅化线程
浏览器最终要把页面变成屏幕上的像素。
这个过程涉及:
把图层内容转换成位图这就叫光栅化。
光栅化工作可以由多个 worker 线程并行完成。
大致流程是:
Render Tree / Layout
↓
Paint 生成绘制指令
↓
图层拆分
↓
Raster Worker 把图层切片转换成位图
↓
Compositor Thread 合成图层
↓
GPU Process 显示到屏幕Worker Threads:Worker 线程
Web Worker 会在独立线程中运行 JS。
例如:
const worker = new Worker('/worker.js')
worker.postMessage({
type: 'calculate',
payload: largeData,
})Worker 线程适合处理:
大量计算
数据压缩
图片处理
复杂算法
不需要直接操作 DOM 的任务
但是 Worker 有一个重要限制:
Web Worker 不能直接访问 DOM因为 DOM 主要归主线程管理。
如果 Worker 可以直接改 DOM,就会产生复杂的线程安全问题。
Network Process:网络进程
Network Process 负责浏览器网络请求相关工作。
包括:
DNS 解析
建立 TCP 连接
TLS 握手
发送 HTTP 请求
接收 HTTP 响应
连接复用
网络缓存相关处理
Cookie 相关处理
代理配置
网络安全策略
当页面执行:
fetch('/api/user')大致流程不是 JS 自己去发网络包,而是:
Renderer Process 中的 JS 发起 fetch
↓
请求通过浏览器内部通信交给 Network Process
↓
Network Process 处理真实网络请求
↓
响应数据返回 Renderer Process
↓
Promise 进入后续异步流程所以:
JS 发起网络请求
不等于 JS 线程自己执行网络 I/O这也是为什么 fetch 是异步的。
GPU Process:GPU 进程
GPU Process 负责和 GPU 通信,处理图形相关任务。
包括:
图层合成
GPU 加速
纹理上传
3D / Canvas / WebGL 相关任务
最终把页面显示到屏幕
浏览器为什么要单独拆 GPU Process?
主要是因为:
GPU 驱动复杂,容易崩溃
图形任务性能要求高
多个页面可以共享 GPU 能力
需要隔离图形相关风险
大致可以这样理解:
Renderer Process 负责生成页面内容和图层信息
Compositor Thread 负责组织合成
GPU Process 负责和 GPU 通信,把结果显示出来Utility Process:工具进程
Utility Process 用来处理一些独立的、可隔离的辅助任务。
比如:
音频处理
视频解码
文件系统访问
数据解压
打印
存储服务
安全敏感任务
这些任务不一定适合放在 Browser Process 或 Renderer Process 中,所以浏览器会拆成独立的 Utility Process。
这样做的好处是:
某个辅助功能出问题
不至于影响整个浏览器Extension Process:扩展进程
如果安装了浏览器插件,比如:
广告拦截插件
翻译插件
密码管理插件
React DevTools
Vue DevTools
浏览器可能会给扩展分配独立进程。
扩展进程可以和页面通信,但会受到浏览器权限系统限制。
这样做也是为了隔离风险:
扩展出问题
不应该直接破坏浏览器主流程四、一个 Tab 一定是一个进程吗
不是。
这是面试里非常容易答错的点。
更准确地说:
一个 Tab 不一定只对应一个进程
一个进程也不一定只服务一个 Tab一个 Tab 可能有多个进程
比如一个页面中有跨站 iframe:
<iframe src="https://a.com/"></iframe>
<iframe src="https://b.com/"></iframe>现代浏览器可能会把不同站点的 iframe 放到不同的 Renderer Process 中。
这叫:
Out-of-Process iframe,简称 OOPIF也就是说:
一个 Tab
├─ 主页面 Renderer Process
├─ a.com iframe Renderer Process
└─ b.com iframe Renderer Process多个 Tab 可能共享某些进程
某些情况下,同站点页面可能复用 Renderer Process。
另外,所有 Tab 通常都会共享:
Browser Process
Network Process
GPU Process
部分 Utility Process
所以不能简单说:
一个 Tab = 一个进程更准确的表达是:
现代浏览器通常会为不同页面或不同站点分配独立的渲染进程,
同时共享浏览器主进程、网络进程、GPU 进程等公共基础进程。五、Site Isolation:站点隔离
Site Isolation 是现代浏览器安全架构中的重要机制。
它的核心目标是:
不同站点尽量运行在不同的渲染进程中注意这里是 site,不是简单的 origin。
origin 是什么
同源策略中的 origin 包括:
协议 + 域名 + 端口例如:
https://example.com:443下面这些都是不同源:
https://example.com
http://example.com
https://api.example.com
https://example.com:3000site 是什么
site 更偏向“站点”概念,通常和可注册域名相关。
例如:
https://www.example.com
https://api.example.com它们可能属于同一个 site:
example.com而:
https://evil.com就是另一个 site。
Site Isolation 解决什么问题
假设你打开了一个恶意页面:
evil.com这个页面又嵌入了银行页面:
<iframe src="https://bank.com/"></iframe>如果两个页面运行在同一个渲染进程里,一旦渲染引擎存在漏洞,恶意页面可能有机会窃取同进程内其他页面的数据。
Site Isolation 会尽量让:
evil.com
bank.com运行在不同的 Renderer Process 里。
这样即使 evil.com 的渲染进程出问题,也更难直接读取 bank.com 的内存数据。
六、JS 单线程和浏览器多进程的关系
这是这一节最重要的理解点。
很多人会混淆:
JS 是单线程
浏览器是多进程
浏览器是多线程这三句话并不矛盾。
JS 单线程指什么
通常说 JS 是单线程,指的是:
在一个页面的渲染进程中,能够操作 DOM 的 JS 主要运行在主线程上。也就是说,同一个页面中:
JS 执行
DOM 修改
样式计算
布局计算
部分事件处理
都和主线程高度相关。
浏览器不是单线程
浏览器整体有很多进程:
Browser Process
Renderer Process
Network Process
GPU Process
Utility Process每个进程内部又有多个线程。
所以浏览器整体一定不是单线程。
Web APIs 不是 JS 主线程自己完成的
例如:
setTimeout(() => {
console.log('timer')
}, 1000)
fetch('/api/user').then(() => {
console.log('done')
})这些任务不是 JS 主线程自己一直等着完成。
大致模型是:
JS 主线程发起异步任务
↓
浏览器其他线程 / 进程处理计时器、网络请求等任务
↓
任务完成后,把回调放入任务队列或微任务队列
↓
JS 主线程空闲后再执行回调所以:
异步不等于多线程执行 JS
异步只是把耗时任务交给浏览器其他能力处理
JS 回调最终仍然要回到 JS 主线程执行为什么 JS 主线程容易成为性能瓶颈
因为主线程负责太多事情:
执行 JS
处理事件
操作 DOM
计算样式
执行布局
触发渲染
执行 rAF 回调
处理微任务如果 JS 长时间占用主线程:
function heavyTask() {
const start = Date.now()
while (Date.now() - start < 5000) {
// 模拟 5 秒 CPU 密集任务
}
}
heavyTask()页面会出现:
点击没反应
输入框卡住
动画停止
页面无法更新
滚动卡顿
原因是:
主线程被 JS 长任务占住
↓
事件无法及时处理
↓
渲染无法及时执行
↓
用户感觉页面卡死为什么 Web Worker 可以优化卡顿
Web Worker 可以把部分计算任务从主线程移走。
主线程
├─ 执行 UI 逻辑
├─ 响应用户输入
├─ 更新 DOM
└─ 渲染页面
Worker 线程
└─ 执行复杂计算适合放到 Worker 的任务:
大数组计算
图片压缩
文件解析
Excel 解析
Markdown 解析
复杂搜索
数据加密解密
大量 JSON 处理
但是 Worker 不适合直接处理 DOM,因为:
Worker 不能访问 DOM它和主线程之间通过消息通信:
// main.ts
const worker = new Worker('/worker.js')
worker.postMessage({
type: 'start',
payload: [1, 2, 3],
})
worker.onmessage = (event) => {
console.log(event.data)
}// worker.js
self.onmessage = (event) => {
const result = event.data.payload.map((item) => item * 2)
self.postMessage(result)
}浏览器多进程架构和页面加载流程的关系
当输入 URL 时,浏览器内部大概这样协作:
用户在地址栏输入 URL
↓
Browser Process 接收输入
↓
Browser Process 判断是搜索词还是 URL
↓
Browser Process 通知 Network Process 发起请求
↓
Network Process 处理 DNS、TCP、TLS、HTTP
↓
响应 HTML 返回
↓
Browser Process 选择或创建 Renderer Process
↓
Renderer Process 解析 HTML、CSS、JS
↓
Renderer Process 生成页面结构、样式、布局、绘制信息
↓
Compositor Thread 组织图层合成
↓
GPU Process 负责最终图形输出
↓
页面显示到屏幕三节内容之间的关系是:
第一节:谁负责?
第二节:页面如何加载?
第三节:页面如何渲染?七、面试问答
Q1:浏览器为什么是多进程架构?
现代浏览器采用多进程架构,主要是为了提升稳定性、安全性和性能。从稳定性上看,不同页面运行在相对独立的渲染进程中,一个页面崩溃通常不会导致整个浏览器崩溃。
从安全性上看,不同站点可以通过 Site Isolation 运行在不同的渲染进程中,配合沙箱机制,降低恶意页面读取其他站点数据的风险。从性能上看,多进程可以更好地利用多核 CPU,同时把浏览器 UI、网络请求、页面渲染、GPU 合成等任务拆分到不同进程中执行。典型的浏览器进程包括 Browser Process、Renderer Process、Network Process、GPU Process 和 Utility Process。
其中 Renderer Process 负责解析 HTML/CSS、执行 JS、构建 DOM、布局和绘制;Network Process 负责网络请求;GPU Process 负责图形合成和 GPU 通信;Browser Process 负责窗口、标签页、导航和进程管理。
Q2:JS 是单线程的,为什么浏览器又是多进程的?
这两个说法不矛盾。JS 单线程通常指的是在一个页面的渲染进程中,能够操作 DOM 的 JS 主要运行在主线程上。这个主线程还负责事件处理、样式计算、布局等任务,所以长时间执行 JS 会阻塞页面响应和渲染。但浏览器整体不是单线程。现代浏览器由多个进程组成,比如浏览器主进程、渲染进程、网络进程、GPU 进程等。每个进程内部也可能有多个线程,比如渲染进程里有主线程、合成线程、光栅化线程和 Worker 线程。所以准确地说,浏览器是多进程多线程架构,而 JS 在单个页面中主要是单线程执行的。


评论
还没有评论
来发表第一条评论吧!