浏览器是如何运作的
浏览器架构
“现代浏览器是多进程多线程架构”指的是像 Chrome、Edge(基于 Chromium)、Firefox 等主流浏览器在设计上采用了 多个操作系统进程(Process),而每个进程中又包含 多个执行线程(Thread) 的结构。这种架构是为了在 稳定性、安全性、性能 三者之间取得最佳平衡。
下面我们从两个层面来理解:
一、什么是“多进程”?为什么用多进程?
✅ 进程(Process)是什么?
- 进程是操作系统分配资源(内存、文件句柄等)的基本单位。
- 每个进程拥有独立的内存空间,彼此隔离。
🌐 浏览器中的主要进程(以 Chrome 为例):
-
浏览器主进程(Browser Process)
- 唯一存在,负责地址栏、书签、标签页管理、窗口 UI。
- 协调其他子进程的创建与销毁。
- 处理
localStorage、IndexedDB等持久化存储。
-
渲染进程(Renderer Process)
- 每个标签页(或站点)通常对应一个独立的渲染进程(启用了 Site Isolation 后更严格)。
- 负责解析 HTML/CSS、执行 JavaScript(V8 引擎)、构建 DOM、布局、绘制。
- 如果页面崩溃,只影响当前标签页,不会导致整个浏览器关闭。
-
GPU 进程(GPU Process)
- 负责硬件加速:合成图层、3D 变换、动画等。
- 利用显卡提升图形性能。
-
网络进程(Network Process)
- 统一处理所有 HTTP/HTTPS 请求、DNS 解析、Cookie 管理、缓存策略。
- 避免网络阻塞主线程。
-
插件/扩展进程(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)模型。我们可以从以下几个方面系统地回答:
一、浏览器是如何渲染页面的?
浏览器渲染页面的过程主要包括以下步骤:
-
解析 HTML 构建 DOM 树
浏览器接收到 HTML 文档后,逐行解析生成 DOM(Document Object Model)节点树。 -
解析 CSS 构建 CSSOM 树
同时解析<style>标签或外部 CSS 文件,生成 CSSOM(CSS Object Model),表示样式规则的树结构。 -
合并 DOM 和 CSSOM 生成 Render Tree(渲染树)
将 DOM 与 CSSOM 结合,过滤掉不可见元素(如display: none),生成包含布局和样式的 Render Tree。 -
Layout(回流/重排)
计算每个 Render Tree 节点在视口中的确切位置和大小(几何信息)。 -
Paint(绘制)
将 Render Tree 的每个节点转换为屏幕上的像素,包括颜色、边框、阴影等。 -
Composite(合成)
如果页面使用了层叠上下文(如transform、opacity等),浏览器会将页面分成多个图层(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):如
setTimeout、setInterval、I/O、UI 渲染等。 - 微任务(Microtask):如
Promise.then/catch/finally、queueMicrotask、MutationObserver。
2. 执行顺序(Event Loop 规则)
每个 Event Loop 循环按如下顺序执行:
- 执行一个宏任务(如 script 整体是一个宏任务)。
- 执行所有当前微任务队列中的任务(清空微任务队列)。
- 进行 UI 渲染(如果需要)。
- 从宏任务队列中取出下一个宏任务执行。
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 线程会捕捉你的输入内容:
- 如果访问的是网址,则 UI 线程会启动一个网络线程来请求 DNS 进行域名解析;接着连接服务器,获取数据;
- 如果你的输入不是网址,浏览器就会使用默认的搜索引擎来查询。
网络线程获取到数据之后会发生什么样的事情?
✅ 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 中。- 伪元素(如
::before的content)会出现在 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 最终将像素渲染到屏幕上,用户看到页面。
🔁 后续交互(如滚动、动画):
- 若仅涉及合成属性(如
transform、opacity),无需主线程参与,由合成器线程直接生成新帧 → 高性能流畅体验。- 若涉及 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)做动画; - 避免在动画中读写
offsetTop、clientWidth等触发强制同步布局(Layout Thrashing)的属性; - 长任务用
requestAnimationFrame或scheduler.yield()(如 React)分片处理; - 利用 Chrome DevTools 的 Performance 面板分析帧耗时,定位瓶颈。