浏览器架构和事件循环

Chrome浏览器架构

Chrome 采用多进程架构,其顶层存在一个 Browser process 用以协调浏览器的其它进程。
image-20210930012419754

Browser Process

  • 子进程的管理(网络、渲染、GPU)
  • 负责包括地址栏,书签栏,前进后退按钮等部分的工作;
  • 负责处理浏览器的一些不可见的底层操作,比如网络请求和文件访问;

Network Service

  • 负责加载网络资源。网络进程内部会启动多个线程来处理不同的网络任务

Renderer Process

  • 负责一个 tab 内关于网页呈现的所有事情,进程启动后,会开启一个渲染主线程,负责执行HTML、CSS、JavaScript代码

Plugin Process

  • 负责控制一个网页用到的所有插件,如 flash

GPU Process

  • 负责处理 GPU 相关的任务

由于一个tab标签页都有一个独立的渲染进程,所以一个tab异常崩溃后,其他tab不会受到影响。

一个渲染进程包括

  • 主线程
  • HTTP请求线程
  • 定时触发线程
  • 事件触发线程
  • GUI线程

渲染进程中的主线程是如何工作的

包括但不限于

  • 解析HTML
  • 解析CSS
  • 计算样式
  • 布局
  • 处理图层
  • 每秒把页面绘制60次
  • 执行全局js代码
  • 执行事件处理函数
  • 执行计时器回调函数

浏览器渲染页面过程

浏览器拿到 HTML 后,并不是一次性把页面画出来,而是经历一条流水线:

1. 解析 HTML,构建 DOM Tree

  • 字节流 -> 字符 -> Token -> Node,最终形成 DOM 树。
  • 解析过程中遇到同步 script 可能会暂停 DOM 构建,先执行 JS。

2. 解析 CSS,构建 CSSOM Tree

  • 下载并解析样式,形成 CSSOM(CSS Object Model)。
  • CSS 解析通常不会阻塞 DOM 解析,但会影响后续渲染阶段。

3. 合并 DOM + CSSOM,生成 Render Tree

  • Render Tree 只包含需要显示的可见节点(如 display: none 不会进入)。
  • 每个可见节点会绑定其计算后的样式信息。

4. Layout(回流 / 重排)

  • 根据视口和样式计算每个节点的几何信息(位置、尺寸)。
  • 这个阶段会产出盒模型坐标数据。

5. Paint(重绘)

  • 把文字、颜色、边框、阴影等绘制成一条条绘制指令(Paint Records)。

6. Layer(分层)

  • 浏览器会根据元素特性把页面拆成多个图层(并不是每个元素都独立成层)。
  • 分层后,后续某些变化只需要更新局部图层,不必整页重绘。

7. Raster(栅格化)

  • 把图层的绘制指令转换为位图纹理(tiles),通常由栅格线程/GPU 参与完成。

8. Composite(合成)

  • 合成线程按图层顺序、透明度、变换矩阵把多个纹理合成,最终显示到屏幕上。

简化理解:DOM + CSSOM -> Render Tree -> Layout -> Paint -> Layer -> Raster -> Composite

常见触发与性能影响

  • 修改几何属性(如 widthheightmargin)更容易触发 Layout + Paint + Composite
  • 修改非几何属性(如 colorbackground)通常触发 Paint + Composite
  • 修改 transformopacity 通常只需 Composite,代价更低。
  • 主线程被长时间 JS 占用时,渲染会被阻塞,页面会出现掉帧或卡顿。

哪些场景更容易触发分层

  • 使用了 3D/变换相关属性(如 transformperspective)。
  • 使用 opacity 动画、will-change 或可能触发独立合成层的特性。
  • position: fixed / stickyvideocanvas 等元素在特定场景下也可能被提升为图层。

注意:分层不是越多越好。图层过多会增加内存占用和合成开销,需要在流畅度和资源之间权衡。

浏览器JS异步执行原理

  1. 所有任务都在主线程上执行,形成一个执行栈。
  2. 主线程之外,还存在一个”任务队列”(task queue)。只要异步任务有了运行结果,就在”任务队列”之中放置一个事件。
  3. 一旦”执行栈”中的所有同步任务执行完毕,系统就会读取”任务队列”。那些对应的异步任务,结束等待状态,进入执行栈并开始执行。
  4. 主线程不断重复上面的第三步。

浏览器中的事件循环

事件循环是浏览器渲染主线程的工作方式,主线程会将不同的任务交给不同线程处理,其他线程处理完后会把回调函数放到对应的队列里

image-20210930190158487

Event Loop(事件循环)中,每一次循环称为 tick, 每一次 tick 的任务如下:

  1. 调用栈执行同步代码,遇到异步任务交给各自的线程处理(其他线程处理完后,会把异步任务的回调加到对应的任务队列中)
  2. 调用栈清空后,检查 Microtask 队列是否存在 Microtask,如果存在则不停的执行,直至清空 Microtask 队列
  3. 更新render,dom渲染(每一次事件循环,浏览器都可能会去更新渲染)
  4. Task 队列中取出一个 Task 执行
  5. 重复以上步骤

任务队列(Task Queue)

延时任务队列(Task)
  • 定时器APIsetTimeoutsetInterval 相应的回调函数
交互任务队列
  • 一段新程序或子程序被直接执行时(比如从一个控制台,或在一个 script 元素中运行代码)。

  • 触发了一个事件,将其回调函数添加到任务队列时,比如交互相关(用户点击、视口变化、键盘事件)

  • 优先级:用户交互相关任务 > 定时任务 > 网络 / 通信任务。

  1. script标签
  2. UI交互(click、mousedown、keydown、touchstart 等)
  3. 网络I/O(XHR/Fetch 完成回调)
  4. setTimeout、setInterval
  5. requestAnimationFrame
  6. MessageChannel 的 onmessage 回调

微任务队列(MicroTask Queue)

  1. MutationObserver
  2. Promise.then catch finally
  3. queueMicrotask()

对于以下demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
new Promise((r) => {
r();
})
.then(() => console.log(1))
.then(() => console.log(2))
.then(() => console.log(3))

new Promise((r) => {
r();
})
.then(() => console.log(4))
.then(() => console.log(5))
.then(() => console.log(6))

// 输出
// 1 4 2 5 3 6

Promise.prototype.then() 会隐式返回一个新 Promise

如果 Promise 的状态是 pending,那么 then 会在该 Promise 上注册一个回调,当其状态发生变化时,对应的回调将作为一个微任务被推入微任务队列

如果 Promise 的状态已经是 fulfilled 或 rejected,那么 then() 会立即创建一个微任务,将传入的对应的回调推入微任务队列

对于以下demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
console.log('start')
new Promise((resolve, reject) => {
setTimeout(() => {
console.log('promise in timeout');
resolve();
});

console.log('promise after timeout');
}).then(() => {
console.log('promise4');
Promise.resolve().then(() => {
console.log('7777')
})
}).then(() => {
console.log('promise5');
});

setTimeout(() => {
console.log('99999');
}, 0);
Promise.resolve().then(() => {
console.log('promise3');
});

console.log('end');
// 输出:
// start
// promise after timeout
// end
// promise3
// promise in timeout
// promise4
// 7777
// promise5
// 99999
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
console.log(1);
async function async1() {
await async2();
console.log(2);
await async3();
console.log(3);
}

async function async2() {
console.log(4);
}

async function async3() {
console.log(5);
}

async1();
console.log(6);
// 1 4 6 2 5 3

Node中的事件循环

Node中的任务队列和异步API

  • Timer队列:定时器相关API,setTimeoutsetInterval
  • Poll队列:IO操作,文件读写、数据库操作、网络请求
  • Check队列setImmediate(node环境中独有)
  • NextTick队列process.nextTick(node环境中独有)

Node中事件循环执行流程

image-20210930190158487

  1. 调用栈执行同步代码,遇到异步任务交给各自的线程处理(其他线程处理完后,会把异步任务的回调加到对应的任务队列中)
  2. 调用栈清空后,执行nextTick队列process.nextTick回调,直至清空nextTick队列
  3. 清空微任务队列Promise.then里的回调
  4. Timer队列里取出setTimeoutsetInterval回调执行,若nextTick队列微任务队列不为空,重复步骤2/3
  5. Poll队列里取出文件IO,网络IO相关操作的回调执行,若nextTick队列微任务队列不为空,重复步骤2/3
  6. Check队列里取出setImmediate回调执行,若nextTick队列微任务队列不为空,重复步骤2/3
  7. Poll队列等待,判断是否有新的异步任务,若Poll队列有任务,直接执行,若其他队列有任务,则继续下一个 Tick

Timer-> Poll -> Check称之为一个Tick
nextTick队列,总会在下个Tick运行之前执行

遇到Poll队列时,会暂停等待另外两个队列,此时如果有新的IO操作,会直接执行(因为Node的设计就是优先处理IO操作

对于以下demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
console.log("sync");
process.nextTick(() => {
console.log("next tick 1");
});

setImmediate(() => {
console.log("setImmediate 1");
});

Promise.resolve().then(() => {
console.log("promise 1");
});

process.nextTick(() => {
console.log("next tick 2");
});

setTimeout(() => {
console.log("setTimeout 1");
}, 0);

setImmediate(() => {
console.log("setImmediate 2");
});

setTimeout(() => {
console.log("setTimeout 2");
}, 0);

Promise.resolve().then(() => {
console.log("promise 2");
});

Promise.resolve().then(() => {
console.log("promise 3");
});

// 输出:
// sync
// next tick 1
// next tick 2
// promise 1
// promise 2
// promise 3
// setTimeout 1
// setTimeout 2
// setImmediate 1
// setImmediate 2

setTimeout和setImmediate的问题

1
2
3
4
5
6
7
8
9
setTimeout(() => {
console.log('定时器')
}, 0)

setImmediate(() => {
console.log('setImmediate')
})

// 输出可能是先“定时器”,也可能是先“setImmediate”

setTimeout在node中延迟时间最小取值是1ms,而不是0ms(最快1ms调用),所以这里谁先输出是不一定的,若要让“setImmediate”先执行,可以包在文件IO的回调里

1
2
3
4
5
6
7
8
9
fs.readFile(_filename, () => {
setTimeout(() => {
console.log('定时器')
}, 0)

setImmediate(() => {
console.log('setImmediate')
})
})