浏览器是如何 “多开” 的?从进程到线程,拆解浏览器的并发逻辑

你有没有想过:为什么 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 绘制。

  • 定时器线程 :处理setTimeoutsetInterval的计时逻辑。

  • 网络线程 :处理fetchXMLHttpRequest等网络请求。

注意 :渲染进程的主线程是单线程的,即 JS 执行和 DOM 渲染共用一个线程。这就是为什么 "JS 长时间运行会阻塞页面渲染" 的原因 ------ 后面会详细解释。

JS 单线程与事件循环:如何处理异步任务?

我们知道,JS 是单线程的 ------ 同一时刻只能执行一个任务。但浏览器中大量操作是异步的(如定时器、网络请求),这是怎么实现的?

答案是:浏览器通过多线程 + 事件循环(Event Loop)机制,让 JS 在单线程下也能处理异步任务

(1)事件循环的核心组件

事件循环涉及以下几个核心组件:

  • 调用栈(Call Stack) :存储正在执行的函数调用。
  • 任务队列(Task Queue) :存储待执行的异步任务回调(分为宏任务队列和微任务队列)。
  • Web API :浏览器提供的 API(如setTimeoutfetchaddEventListener),由浏览器的其他线程实现。

执行流程

  1. JS 主线程从调用栈中执行同步任务。
  2. 遇到异步任务(如setTimeoutfetch)时,将任务交给对应的 Web API 线程处理。
  3. 当异步任务完成(如定时器时间到、网络请求返回),Web API 线程将回调函数放入任务队列。
  4. 当调用栈为空时,事件循环从任务队列中取出一个任务放入调用栈执行。

浏览器中的异步线程支持

虽然 JS 主线程是单线程的,但浏览器通过其他线程支持异步操作:

  • 定时器线程 :处理setTimeoutsetInterval的计时。当时间到达时,将回调函数放入宏任务队列。
  • 网络线程 :处理fetchXMLHttpRequest等网络请求。请求完成后,将回调函数放入宏任务队列。
  • 事件监听线程 :监听 DOM 事件(如clickscroll)。事件触发时,将回调函数放入宏任务队列。

例如,当执行setTimeout(() => console.log('done'), 1000)时:

  1. 主线程遇到setTimeout,将其交给浏览器的定时器线程。
  2. 定时器线程开始计时,主线程继续执行后续代码。
  3. 1000ms 后,定时器线程将回调函数() => console.log('done')放入宏任务队列。
  4. 当主线程调用栈为空时,事件循环从宏任务队列取出该回调执行。

渲染与 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';
});

执行过程

  1. 用户点击按钮,触发事件回调。
  2. 主线程执行longTask,耗时几百毫秒甚至更长。
  3. 期间浏览器无法执行渲染,页面冻结(无法响应其他事件)。
  4. longTask执行完后,主线程更新 DOM,页面才恢复响应。

(3)如何避免长任务卡顿?

  • 拆分长任务 :将耗时操作拆分成多个小任务,使用requestAnimationFramesetTimeout分批执行。

    javascript 复制代码
    function 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);
    };

总结

  1. 进程与线程的区别
  • 进程是资源分配的最小单位,线程是 CPU 调度的最小单位。
  • 进程间资源隔离,通信复杂;同一进程内的线程共享资源,通信高效。
  1. Chrome 多进程架构的优势
  • 隔离性:一个页面崩溃不影响其他页面。
  • 安全性:渲染进程可以运行在沙箱中。
  • 性能优化:不同类型的任务由不同进程处理。
  1. JS 单线程与异步处理
  • JS 主线程是单线程的,但浏览器通过多线程(如定时器线程、网络线程)支持异步操作。
  • 事件循环机制负责将异步任务的回调放入任务队列,并在主线程空闲时执行。
  1. 微任务与宏任务的执行顺序
  • 微任务(如Promise.then)优先级高于宏任务(如setTimeout)。
  • 每次调用栈清空后,优先处理所有微任务,再执行一个宏任务。
  1. 渲染与 JS 执行的关系
  • 两者互斥,JS 长时间运行会阻塞渲染,导致页面卡顿。
  • 可以通过拆分长任务、使用 Web Workers 等方式优化。
相关推荐
爱编程的喵7 分钟前
前端路由深度解析:从传统页面到SPA的完美蜕变
前端·react.js·html
加油乐11 分钟前
js及vue主题切换方案
前端·javascript·vue.js
帅夫帅夫15 分钟前
前端小白也能看懂的 Promise 原理与使用教程(附 async/await 升级指南)
前端
用户498107278023015 分钟前
浏览器原生支持的组件化方案?Web Components深度解毒指南
前端
每天都想睡觉的190015 分钟前
实现一个 React 版本的 Keep-Alive 组件,并支持 Tab 管理、缓存、关闭等功能
前端·react.js
轻语呢喃20 分钟前
前端路由:从传统页面跳转到单页应用(SPA)
前端·react.js·html
顾林海24 分钟前
Android深入解析 so 文件体积优化
android·面试·性能优化
foxhuli22925 分钟前
echarts 绘制3D中国地图
前端
KeyNG_Jykxg26 分钟前
🥳Elx开源升级:XMarkdown 组件加入、Storybook 预览体验升级
前端·vue.js·人工智能