你有没有想过:为什么 Chrome 同时开 10 个网页也不会卡死?为什么一个页面崩溃了,其他页面还能正常运行?这背后的秘密,就藏在现代浏览器的 "多进程架构" 里,今天让我们一起来拆解它神秘的背后。
先搞懂:CPU 如何同时处理多个任务?
现代操作系统(如 Windows、macOS)都支持 "多任务"------ 你可以一边听歌、一边写文档、一边刷网页。但 CPU 核心数是有限的(比如 4 核、8 核),它是怎么做到 "同时" 处理这么多任务的?
答案是:CPU 通过 "时间片轮转"(轮循)技术,在多个任务间快速切换。比如有 3 个任务 A、B、C,CPU 会给每个任务分配一个 "时间片"(比如 20 毫秒):
- 0~20ms:执行任务 A;
- 20~40ms:执行任务 B;
- 40~60ms:执行任务 C;
- 60~80ms:回到任务 A 继续执行......
由于切换速度极快(每秒几十次),我们感觉多个任务在 "同时" 执行,但实际上 CPU 在同一时刻只执行一个任务。这就是 并发(Concurrency) 的本质 ------ 通过快速切换,让多个任务看起来在同时运行。
进程 vs 线程:谁才是真正的 "干活的"?
在操作系统中,"进程" 和 "线程" 是实现并发的两个核心概念,它们的关系可以用一句话概括:进程是资源分配的最小单位,线程是 CPU 调度的最小单位。
(1)进程:程序运行的 "容器"
进程是 "程序在操作系统中的一次执行实例",它包含了程序运行所需的所有资源:
- 独立的内存空间(包含代码、数据、堆栈等);
- 独立的系统资源(如文件句柄、网络连接等);
- 唯一的进程 ID(PID)。
特点:
- 资源隔离:不同进程的内存空间相互隔离,一个进程无法直接访问另一个进程的数据。
- 开销大:创建和销毁进程需要分配和回收大量资源,成本较高。
- 进程间通信(IPC)复杂:由于资源隔离,进程间通信需要通过专门的机制(如管道、消息队列、共享内存等)。
(2)线程:进程中的 "执行单元"
线程是进程内部的一个 "执行流",一个进程可以包含多个线程。所有线程共享进程的资源(如内存、文件句柄),但每个线程有自己独立的调用栈、寄存器和局部变量。
比如 Chrome 进程中,可能有专门处理网络请求的线程、处理渲染的线程、执行 JS 的线程等。这些线程协同工作,共同完成浏览器的各种功能。
特点:
- 轻量级:创建和销毁线程的开销比进程小得多。
- 共享资源:同一进程内的线程可以直接访问共享内存,通信效率高。
- 协作高效:多个线程可以并行执行,提高程序的吞吐量。
但多线程也有风险:如果多个线程同时修改共享数据,可能导致数据竞争(Race Condition),需要通过同步机制(如锁)来解决。
(3)进程与线程的对比表
对比项 | 进程 | 线程 |
---|---|---|
资源分配 | 独立分配内存、文件句柄等 | 共享所属进程的资源 |
通信方式 | 通过 IPC(管道、消息队列) | 直接访问共享内存 |
创建 / 销毁开销 | 高 | 低 |
切换开销 | 高 | 低 |
并发性 | 多个进程并发执行 | 同一进程内的多线程并发 |
健壮性 | 一个进程崩溃不影响其他 | 一个线程崩溃可能导致整个进程崩溃 |
浏览器的多进程架构(以 Chrome 为例)
早期的浏览器(如 IE6)是 "单进程多线程" 架构 ------ 所有功能(如渲染、JS 执行、网络请求)都在一个进程内的不同线程中执行。这种架构的问题是:一个页面崩溃会导致整个浏览器崩溃,且多个页面之间会竞争资源,容易卡顿。 Chrome 从 2008 年发布就采用了 "多进程架构",核心设计如下:
(1)Chrome 的主要进程
Chrome 浏览器通常包含以下几种进程:
- 浏览器主进程:负责浏览器界面(地址栏、书签、标签管理等)、子进程管理(创建 / 销毁渲染进程)、存储(如 Cookie、缓存)等。
- 渲染进程:每个 Tab 标签页对应一个渲染进程,负责 HTML、CSS、JS 的解析和渲染。
- 网络进程:负责网络请求(如 HTTP 请求、WebSocket)。
- GPU 进程:负责图形渲染、加速(如 3D 变换、Canvas 绘制)。
- 插件进程:每个插件(如 Flash)对应一个进程,负责插件的运行。
核心优势:
- 隔离性:一个渲染进程崩溃只会影响当前 Tab,其他 Tab 不受影响。
- 安全性:渲染进程可以运行在沙箱中,限制对系统资源的访问。
- 性能优化:不同类型的任务(如渲染、网络、GPU)由不同进程处理,避免资源竞争。
(2)渲染进程内部:多线程协作
每个渲染进程是一个 "多线程" 环境,主要包含以下线程:
-
主线程:
- 解析 HTML、CSS,构建 DOM 树和 CSSOM 树;
- 合并 DOM 树和 CSSOM 树,生成渲染树;
- 执行布局(Layout),计算每个元素的位置和大小;
- 执行 JS 代码(如 React、Vue 的渲染逻辑)。
-
合成线程:负责将渲染树分层,生成合成层,并将它们发送到 GPU 进程进行绘制。
-
光栅化线程:将合成层转换为位图(Bitmaps),供 GPU 绘制。
-
定时器线程 :处理
setTimeout
、setInterval
的计时逻辑。 -
网络线程 :处理
fetch
、XMLHttpRequest
等网络请求。
注意 :渲染进程的主线程是单线程的,即 JS 执行和 DOM 渲染共用一个线程。这就是为什么 "JS 长时间运行会阻塞页面渲染" 的原因 ------ 后面会详细解释。
JS 单线程与事件循环:如何处理异步任务?
我们知道,JS 是单线程的 ------ 同一时刻只能执行一个任务。但浏览器中大量操作是异步的(如定时器、网络请求),这是怎么实现的?
答案是:浏览器通过多线程 + 事件循环(Event Loop)机制,让 JS 在单线程下也能处理异步任务。
(1)事件循环的核心组件
事件循环涉及以下几个核心组件:
- 调用栈(Call Stack) :存储正在执行的函数调用。
- 任务队列(Task Queue) :存储待执行的异步任务回调(分为宏任务队列和微任务队列)。
- Web API :浏览器提供的 API(如
setTimeout
、fetch
、addEventListener
),由浏览器的其他线程实现。
执行流程:
- JS 主线程从调用栈中执行同步任务。
- 遇到异步任务(如
setTimeout
、fetch
)时,将任务交给对应的 Web API 线程处理。 - 当异步任务完成(如定时器时间到、网络请求返回),Web API 线程将回调函数放入任务队列。
- 当调用栈为空时,事件循环从任务队列中取出一个任务放入调用栈执行。
浏览器中的异步线程支持
虽然 JS 主线程是单线程的,但浏览器通过其他线程支持异步操作:
- 定时器线程 :处理
setTimeout
和setInterval
的计时。当时间到达时,将回调函数放入宏任务队列。 - 网络线程 :处理
fetch
、XMLHttpRequest
等网络请求。请求完成后,将回调函数放入宏任务队列。 - 事件监听线程 :监听 DOM 事件(如
click
、scroll
)。事件触发时,将回调函数放入宏任务队列。
例如,当执行setTimeout(() => console.log('done'), 1000)
时:
- 主线程遇到
setTimeout
,将其交给浏览器的定时器线程。 - 定时器线程开始计时,主线程继续执行后续代码。
- 1000ms 后,定时器线程将回调函数
() => console.log('done')
放入宏任务队列。 - 当主线程调用栈为空时,事件循环从宏任务队列取出该回调执行。
渲染与 JS 执行的互斥关系:为什么会卡顿?
前面提到,渲染进程的主线程既是 JS 执行线程,也是 DOM 渲染线程。这就导致一个问题:JS 执行和 DOM 渲染不能同时进行,它们是互斥的。
(1)为什么互斥?
如果 JS 和渲染可以同时进行,会出现以下问题:
- JS 修改 DOM 结构(如删除一个元素),同时渲染线程正在绘制这个元素,导致渲染错误。
- JS 修改 CSS 样式(如改变元素宽度),同时渲染线程正在计算布局,导致布局错乱。
为了避免这些问题,浏览器设计为:当 JS 执行时,渲染暂停;当渲染进行时,JS 暂停。
(2)长任务导致的卡顿
如果 JS 代码中有耗时操作(如复杂计算、大量 DOM 操作),会导致主线程长时间被占用,无法执行渲染,从而出现页面卡顿。
示例:
javascript
// 耗时操作:循环1亿次
function longTask() {
let sum = 0;
for (let i = 0; i < 100000000; i++) {
sum += i;
}
return sum;
}
// 点击按钮触发长任务
document.getElementById('btn').addEventListener('click', () => {
longTask();
// 长任务执行完后才会更新DOM
document.getElementById('result').textContent = 'Done';
});
执行过程:
- 用户点击按钮,触发事件回调。
- 主线程执行
longTask
,耗时几百毫秒甚至更长。 - 期间浏览器无法执行渲染,页面冻结(无法响应其他事件)。
longTask
执行完后,主线程更新 DOM,页面才恢复响应。
(3)如何避免长任务卡顿?
-
拆分长任务 :将耗时操作拆分成多个小任务,使用
requestAnimationFrame
或setTimeout
分批执行。javascriptfunction splitLongTask() { const total = 1000000; let processed = 0; function processChunk() { // 每次处理1000个 for (let i = 0; i < 1000 && processed < total; i++) { // 处理逻辑... processed++; } // 如果还有剩余,继续下一批 if (processed < total) { requestAnimationFrame(processChunk); } } // 开始处理 processChunk(); }
-
Web Workers:将耗时计算放到 Web Worker 中执行,不阻塞主线程。
javascript// 主线程 const worker = new Worker('worker.js'); worker.postMessage('开始计算'); worker.onmessage = (e) => { console.log('计算结果:', e.data); }; // worker.js self.onmessage = () => { // 在worker中执行耗时计算 const result = performHeavyCalculation(); self.postMessage(result); };
总结
- 进程与线程的区别:
- 进程是资源分配的最小单位,线程是 CPU 调度的最小单位。
- 进程间资源隔离,通信复杂;同一进程内的线程共享资源,通信高效。
- Chrome 多进程架构的优势:
- 隔离性:一个页面崩溃不影响其他页面。
- 安全性:渲染进程可以运行在沙箱中。
- 性能优化:不同类型的任务由不同进程处理。
- JS 单线程与异步处理:
- JS 主线程是单线程的,但浏览器通过多线程(如定时器线程、网络线程)支持异步操作。
- 事件循环机制负责将异步任务的回调放入任务队列,并在主线程空闲时执行。
- 微任务与宏任务的执行顺序:
- 微任务(如
Promise.then
)优先级高于宏任务(如setTimeout
)。 - 每次调用栈清空后,优先处理所有微任务,再执行一个宏任务。
- 渲染与 JS 执行的关系:
- 两者互斥,JS 长时间运行会阻塞渲染,导致页面卡顿。
- 可以通过拆分长任务、使用 Web Workers 等方式优化。