跳到主要内容

浏览器是如何运作的

浏览器架构

“现代浏览器是多进程多线程架构”指的是像 Chrome、Edge(基于 Chromium)、Firefox 等主流浏览器在设计上采用了 多个操作系统进程(Process),而每个进程中又包含 多个执行线程(Thread) 的结构。这种架构是为了在 稳定性、安全性、性能 三者之间取得最佳平衡。

下面我们从两个层面来理解:


一、什么是“多进程”?为什么用多进程?

✅ 进程(Process)是什么?

  • 进程是操作系统分配资源(内存、文件句柄等)的基本单位。
  • 每个进程拥有独立的内存空间,彼此隔离。

🌐 浏览器中的主要进程(以 Chrome 为例):

  1. 浏览器主进程(Browser Process)

    • 唯一存在,负责地址栏、书签、标签页管理、窗口 UI。
    • 协调其他子进程的创建与销毁。
    • 处理 localStorageIndexedDB 等持久化存储。
  2. 渲染进程(Renderer Process)

    • 每个标签页(或站点)通常对应一个独立的渲染进程(启用了 Site Isolation 后更严格)。
    • 负责解析 HTML/CSS、执行 JavaScript(V8 引擎)、构建 DOM、布局、绘制。
    • 如果页面崩溃,只影响当前标签页,不会导致整个浏览器关闭。
  3. GPU 进程(GPU Process)

    • 负责硬件加速:合成图层、3D 变换、动画等。
    • 利用显卡提升图形性能。
  4. 网络进程(Network Process)

    • 统一处理所有 HTTP/HTTPS 请求、DNS 解析、Cookie 管理、缓存策略。
    • 避免网络阻塞主线程。
  5. 插件/扩展进程(Plugin / Extension Process)(逐渐减少)

    • 第三方插件(如旧版 Flash)或扩展运行在独立沙箱中,防止恶意代码破坏系统。

🔒 多进程的优势:

优势说明
稳定性一个页面崩溃(如 JS 死循环、插件 bug),只关闭对应标签页,不影响其他页面或浏览器本身。
安全性渲染进程运行在“沙箱”中,无法直接访问磁盘、摄像头、系统文件等敏感资源。
性能隔离视频页、游戏页等高负载页面不会拖慢文档编辑页或后台标签。

💡 类比:把浏览器想象成一家公司——主进程是 CEO,每个部门(渲染、网络、GPU)是独立子公司,互不干扰。


二、什么是“多线程”?为什么用多线程?

✅ 线程(Thread)是什么?

  • 线程是 CPU 执行代码的最小单位。
  • 同一进程内的多个线程共享内存空间,但有各自的执行栈。
  • 线程切换开销小,适合并发任务。

🧵 浏览器进程内部的典型线程(以渲染进程为例):

虽然 JavaScript 是单线程的,但渲染进程内部其实是多线程协作

线程职责
主线程(Main Thread)执行 JS、解析 HTML/CSS、构建 DOM/CSSOM、Layout、Paint(生成绘制指令)。这是开发者最关心的线程。
合成线程(Compositor Thread)接收主线程的绘制指令,负责图层合成、滚动、transform/opacity 动画,即使主线程卡死也能保持流畅滚动。
光栅化线程(Raster Thread)将矢量绘制命令转换为像素位图(光栅化),可并行处理多个图块(tiles)。
Worker 线程Web Worker 创建,用于执行耗时计算(如图像处理、加密),不阻塞主线程。

⚠️ 注意:JavaScript 引擎(如 V8)只在主线程运行,所以 JS 本身是单线程的(除非使用 Web Worker)。


三、如何直观理解“多进程 + 多线程”?

举个例子:

你打开了三个标签页:

  • 标签 1:YouTube(播放视频)
  • 标签 2:Gmail(收发邮件)
  • 标签 3:一个写崩了的测试页面(含死循环 JS)

在多进程架构下:

  • 每个标签页运行在独立的渲染进程中。
  • YouTube 的视频解码可能用到 GPU 进程;
  • Gmail 的邮件请求通过网络进程发送;
  • 崩溃的测试页只导致自己的渲染进程退出,其他两个标签照常工作。

而在每个渲染进程中:

  • 主线程执行页面 JS;
  • 合成线程让 YouTube 的播放控件滑动流畅;
  • 光栅线程在后台把邮件界面转换成像素。

四、对比:早期浏览器(如 IE6) vs 现代浏览器

特性早期浏览器(单进程)现代浏览器(多进程多线程)
架构单进程 + 多线程多进程 + 每个进程内多线程
稳定性一个页面崩溃 → 整个浏览器闪退页面隔离,互不影响
安全性插件可直接访问系统资源沙箱隔离,权限受限
性能所有页面争抢同一资源资源按需分配,优先级调度

总结一句话:

“多进程”解决的是“模块隔离”问题(防崩溃、保安全),“多线程”解决的是“任务并行”问题(提性能、保流畅)。两者结合,让现代浏览器既强大又可靠。


浏览器渲染流程

这是一个经典的前端面试题,涉及浏览器渲染机制、多线程协作以及 JavaScript 的事件循环(Event Loop)模型。我们可以从以下几个方面系统地回答:


一、浏览器是如何渲染页面的?

浏览器渲染页面的过程主要包括以下步骤:

  1. 解析 HTML 构建 DOM 树
    浏览器接收到 HTML 文档后,逐行解析生成 DOM(Document Object Model)节点树。

  2. 解析 CSS 构建 CSSOM 树
    同时解析 <style> 标签或外部 CSS 文件,生成 CSSOM(CSS Object Model),表示样式规则的树结构。

  3. 合并 DOM 和 CSSOM 生成 Render Tree(渲染树)
    将 DOM 与 CSSOM 结合,过滤掉不可见元素(如 display: none),生成包含布局和样式的 Render Tree。

  4. Layout(回流/重排)
    计算每个 Render Tree 节点在视口中的确切位置和大小(几何信息)。

  5. Paint(绘制)
    将 Render Tree 的每个节点转换为屏幕上的像素,包括颜色、边框、阴影等。

  6. Composite(合成)
    如果页面使用了层叠上下文(如 transformopacity 等),浏览器会将页面分成多个图层(Layers),分别绘制后再合成最终画面。

📌 注意:JS 执行可能阻塞 DOM 构建(如果 script 是同步的),而 CSS 会阻塞 Render Tree 的构建。


二、浏览器有哪些线程配合工作?

现代浏览器是多进程多线程架构(以 Chromium 为例):

主要线程包括:

线程作用
主线程(Main Thread)执行 JS、构建 DOM/CSSOM、Layout、Paint(部分)、处理用户交互等。JavaScript 是单线程运行在此线程上。
Compositor 线程(合成线程)负责图层合成、滚动、动画(如 transform/opacity 动画)等,不阻塞主线程。
Raster 线程(光栅化线程)将绘制命令转换为 GPU 可理解的位图(光栅化)。
Worker 线程Web Worker 创建的独立 JS 执行环境,可并行执行计算任务,不阻塞主线程。
网络线程(Network Thread)处理 HTTP 请求、资源加载等。
GPU 线程与 GPU 通信,加速图形渲染。

⚠️ JavaScript 引擎(如 V8)运行在主线程,因此 JS 是单线程的(除 Web Worker 外)。


三、Promise 和定时器如何在线程中配合执行?

虽然浏览器有多个线程,但 JavaScript 代码始终在主线程执行。Promise 和定时器的“异步”行为依赖于 事件循环(Event Loop)任务队列(Task Queues)

1. 任务分类

  • 宏任务(Macrotask):如 setTimeoutsetInterval、I/O、UI 渲染等。
  • 微任务(Microtask):如 Promise.then/catch/finallyqueueMicrotaskMutationObserver

2. 执行顺序(Event Loop 规则)

每个 Event Loop 循环按如下顺序执行:

  1. 执行一个宏任务(如 script 整体是一个宏任务)。
  2. 执行所有当前微任务队列中的任务(清空微任务队列)。
  3. 进行 UI 渲染(如果需要)。
  4. 从宏任务队列中取出下一个宏任务执行。

3. 示例说明

console.log('1');

setTimeout(() => console.log('2'), 0);

Promise.resolve().then(() => console.log('3'));

console.log('4');

输出:

1
4
3
2
  • '1''4' 是同步代码(主宏任务)。
  • Promise.then 是微任务,在当前宏任务结束后立即执行。
  • setTimeout 是宏任务,要等到下一轮 Event Loop。

4. 线程协作视角

  • setTimeout:由浏览器定时器线程计时,时间到后将回调放入宏任务队列,等待主线程 Event Loop 处理。
  • Promise:状态变更(resolve/reject)是同步的,.then() 回调被放入微任务队列,由主线程在当前任务结束后立即执行。
  • 所有回调最终都在主线程执行,其他线程只负责“触发”或“通知”。

总结

  • 浏览器渲染涉及 DOM/CSSOM → Render Tree → Layout → Paint → Composite。
  • 多线程协作:主线程(JS + 渲染核心)、合成线程、网络线程、Worker 线程等。
  • JS 是单线程的,异步靠 Event Loop + 任务队列实现。
  • Promise(微任务)优先于 setTimeout(宏任务)执行。
  • 所有 JS 回调最终在主线程执行,其他线程仅辅助调度。

常见问题

当你在地址栏输入地址时,浏览器内部执行了什么操作?

A: 当你在地址栏输入地址时,浏览器进程的 UI 线程会捕捉你的输入内容:

  1. 如果访问的是网址,则 UI 线程会启动一个网络线程来请求 DNS 进行域名解析;接着连接服务器,获取数据;
  2. 如果你的输入不是网址,浏览器就会使用默认的搜索引擎来查询。

网络线程获取到数据之后会发生什么样的事情?


1. 网络请求与安全校验

  • 浏览器主进程(UI 线程)发起页面请求。
  • 网络线程负责下载 HTML 资源。
  • 下载完成后,通过 SafeBrowsing 等安全机制检查站点是否恶意。
    • 若为危险站点,展示警告页(可选择继续访问)。
  • 安全校验通过后,网络线程通知 UI 线程准备渲染。

2. 创建渲染器进程 & 数据传递

  • UI 线程创建独立的 渲染器进程(Renderer Process)
  • 通过 IPC(进程间通信) 将 HTML 数据传给渲染器进程。
  • 渲染器进程开始接管页面构建任务。

3. 构建 DOM 树(解析 HTML)

  • 渲染器进程的主线程解析 HTML:
    • 通过 Tokenizer(标记化)Parser(解析器) 将 HTML 转为标记(tokens)。
    • 构建 DOM 树,以 document 为根节点。
  • 遇到 <script> 标签时会阻塞 HTML 解析(因 JS 可能修改 DOM,如 document.write)。
    • 使用 async / defer 可避免阻塞。

📌 图片、CSS 等资源不阻塞 DOM 构建(但 CSS 会阻塞 Render Tree 生成)。


4. 计算样式 & 构建 Layout Tree(布局)

  • 渲染器进程的主线程解析所有 CSS(包括浏览器默认样式),计算每个 DOM 节点的 最终样式(Computed Style)
  • 基于 DOM + 样式,进行 Layout(回流/重排)
    • 计算每个可见元素的几何信息(位置 x/y、宽高)。
    • 生成 Layout Tree(布局树)

⚠️ 注意:

  • display: none 的元素不出现在 Layout Tree 中。
  • 伪元素(如 ::beforecontent会出现在 Layout Tree,但不在 DOM 中。

5. 生成绘制顺序(Paint)

  • 主线程遍历 Layout Tree,确定绘制顺序(考虑 z-index、层叠上下文等)。
  • 生成 Paint Records(绘制记录),记录“先画谁、后画谁”。

6. 分层、栅格化与合成(Compositing)

  • 主线程根据样式(如 transform, opacity, will-change)将页面划分为多个 图层(Layers),生成 Layer Tree
  • Layer Tree 和 Paint Records 通过 IPC 交给 合成器线程(Compositor Thread)
  • 合成器线程:
    • 将每个图层切分为多个 图块(Tiles)
    • 将图块分发给 栅格化线程(Raster Thread) 进行 栅格化(Rasterization) → 转为像素位图。
    • 栅格化结果存入 GPU 内存
  • 合成器线程生成 合成器帧(Compositor Frame),包含各图块的绘制位置信息(Draw Quads)。

7. 提交帧并显示到屏幕

  • 合成器帧通过 IPC 传回 浏览器主进程
  • 浏览器进程将帧提交给 GPU
  • GPU 最终将像素渲染到屏幕上,用户看到页面。

🔁 后续交互(如滚动、动画)

  • 若仅涉及合成属性(如 transformopacity),无需主线程参与,由合成器线程直接生成新帧 → 高性能流畅体验。
  • 若涉及 DOM 或样式变更,则重新触发上述部分或全部流程。

🧩 总结图示(简化流程):

网络请求 → 安全校验 → 创建渲染进程 → 

HTML → DOM Tree → CSS → Computed Style →

Layout Tree → Paint Records → Layer Tree →

合成器线程 → 栅格化(Tiles)→ GPU → 屏幕显示

重排和重绘

1. 问题根源:主线程资源竞争导致掉帧

  • 重排(Reflow):修改元素尺寸、位置等几何属性 → 触发 样式计算 → 布局 → 绘制 → 合成,开销大。
  • 重绘(Repaint):修改颜色等非几何属性 → 触发 样式计算 + 绘制,开销较小。
  • JavaScript、样式计算、布局、绘制 都运行在渲染器进程的主线程上。
  • 浏览器目标是 60 FPS(每帧 ≈ 16.67ms)
  • 若 JS 任务过长,会挤占布局/绘制时间,导致下一帧无法按时渲染动画卡顿、掉帧

2. 优化方案一:使用 requestAnimationFrame() 拆分 JS 任务

  • requestAnimationFrame(callback) 会在每一帧开始前被浏览器调用。
  • 开发者可将长耗时 JS 任务拆分成小块,每帧只执行一部分。
  • 当前帧时间用完前主动暂停,归还主线程给渲染流程(布局/绘制)。
  • 确保渲染优先级,避免阻塞下一帧。
  • ✅ 典型应用:React Fiber 架构利用此机制实现可中断的协调(reconciliation),提升响应性。

📌 核心思想:协作式调度(Cooperative Scheduling) —— JS 主动让出主线程。


3. 优化方案二:使用 transform / opacity 实现合成动画

  • 修改 transform(如 translate, scale, rotate)或 opacity
    • 不会触发重排或重绘
    • 浏览器会自动将该元素提升为独立图层(Layer Promotion)
    • 动画过程完全由 合成器线程(Compositor Thread) 处理;
    • 无需主线程参与,不与 JS 抢夺资源。
  • 动画直接在 GPU 上合成,性能极高,适合复杂交互动画。

⚠️ 注意:确保元素被成功提升为合成层(可通过 DevTools 的 Layers 面板验证)。


4. 总结对比:两种优化的本质区别

优化方式适用场景是否占用主线程核心优势
requestAnimationFrame + 任务拆分需要 JS 控制的复杂逻辑(如数据处理、状态更新)✅ 占用,但可控、分帧执行让 JS 与渲染协作共存,避免长时间阻塞
transform / opacity 合成动画纯视觉动画(位移、缩放、淡入淡出等)不占用主线程完全绕过样式/布局/绘制,极致流畅

💡 最佳实践建议:

  • 优先使用 CSS 合成属性transform, opacity)做动画;
  • 避免在动画中读写 offsetTopclientWidth 等触发强制同步布局(Layout Thrashing)的属性
  • 长任务用 requestAnimationFramescheduler.yield()(如 React)分片处理
  • 利用 Chrome DevTools 的 Performance 面板分析帧耗时,定位瓶颈。