浏览器架构和事件循环
Chrome浏览器架构
Chrome 采用多进程架构,其顶层存在一个 Browser process 用以协调浏览器的其它进程。
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。
常见触发与性能影响
- 修改几何属性(如
width、height、margin)更容易触发 Layout + Paint + Composite。 - 修改非几何属性(如
color、background)通常触发 Paint + Composite。 - 修改
transform、opacity通常只需 Composite,代价更低。 - 主线程被长时间 JS 占用时,渲染会被阻塞,页面会出现掉帧或卡顿。
哪些场景更容易触发分层
- 使用了 3D/变换相关属性(如
transform、perspective)。 - 使用
opacity动画、will-change或可能触发独立合成层的特性。 position: fixed/sticky、video、canvas等元素在特定场景下也可能被提升为图层。
注意:分层不是越多越好。图层过多会增加内存占用和合成开销,需要在流畅度和资源之间权衡。
浏览器JS异步执行原理
- 所有任务都在主线程上执行,形成一个执行栈。
- 主线程之外,还存在一个”任务队列”(task queue)。只要异步任务有了运行结果,就在”任务队列”之中放置一个事件。
- 一旦”执行栈”中的所有同步任务执行完毕,系统就会读取”任务队列”。那些对应的异步任务,结束等待状态,进入执行栈并开始执行。
- 主线程不断重复上面的第三步。
浏览器中的事件循环
事件循环是浏览器渲染主线程的工作方式,主线程会将不同的任务交给不同线程处理,其他线程处理完后会把回调函数放到对应的队列里

Event Loop(事件循环)中,每一次循环称为 tick, 每一次 tick 的任务如下:
- 调用栈执行同步代码,遇到异步任务交给各自的线程处理(其他线程处理完后,会把异步任务的回调加到对应的任务队列中)
- 调用栈清空后,检查
Microtask队列是否存在Microtask,如果存在则不停的执行,直至清空Microtask队列 - 更新render,dom渲染(每一次事件循环,浏览器都可能会去更新渲染)
- 从
Task队列中取出一个Task执行 - 重复以上步骤
任务队列(Task Queue)
延时任务队列(Task)
- 定时器API
setTimeout和setInterval相应的回调函数
交互任务队列
一段新程序或子程序被直接执行时(比如从一个控制台,或在一个 script 元素中运行代码)。
触发了一个事件,将其回调函数添加到任务队列时,比如交互相关(用户点击、视口变化、键盘事件)
优先级:用户交互相关任务 > 定时任务 > 网络 / 通信任务。
script标签UI交互(click、mousedown、keydown、touchstart 等)网络I/O(XHR/Fetch 完成回调)setTimeout、setIntervalrequestAnimationFrameMessageChannel 的 onmessage 回调
微任务队列(MicroTask Queue)
MutationObserverPromise.then catch finallyqueueMicrotask()
对于以下demo
1 | new Promise((r) => { |
Promise.prototype.then() 会隐式返回一个新 Promise
如果 Promise 的状态是 pending,那么 then 会在该 Promise 上注册一个回调,当其状态发生变化时,对应的回调将作为一个微任务被推入微任务队列
如果 Promise 的状态已经是 fulfilled 或 rejected,那么 then() 会立即创建一个微任务,将传入的对应的回调推入微任务队列
对于以下demo:
1 | console.log('start') |
1 | console.log(1); |
Node中的事件循环
Node中的任务队列和异步API
- Timer队列:定时器相关API,
setTimeout、setInterval - Poll队列:IO操作,文件读写、数据库操作、网络请求
- Check队列:
setImmediate(node环境中独有) - NextTick队列:
process.nextTick(node环境中独有)
Node中事件循环执行流程

- 调用栈执行同步代码,遇到异步任务交给各自的线程处理(其他线程处理完后,会把异步任务的回调加到对应的任务队列中)
- 调用栈清空后,执行nextTick队列里
process.nextTick回调,直至清空nextTick队列 - 清空微任务队列中
Promise.then里的回调 - 从Timer队列里取出
setTimeout,setInterval回调执行,若nextTick队列和微任务队列不为空,重复步骤2/3 - 从Poll队列里取出文件IO,网络IO相关操作的回调执行,若nextTick队列和微任务队列不为空,重复步骤2/3
- 从Check队列里取出
setImmediate回调执行,若nextTick队列和微任务队列不为空,重复步骤2/3 - 在Poll队列等待,判断是否有新的异步任务,若Poll队列有任务,直接执行,若其他队列有任务,则继续下一个 Tick
Timer-> Poll -> Check称之为一个Tick
nextTick队列,总会在下个Tick运行之前执行
遇到Poll队列时,会暂停等待另外两个队列,此时如果有新的IO操作,会直接执行(因为Node的设计就是优先处理IO操作)
对于以下demo:
1 | console.log("sync"); |
setTimeout和setImmediate的问题
1 | setTimeout(() => { |
setTimeout在node中延迟时间最小取值是1ms,而不是0ms(最快1ms调用),所以这里谁先输出是不一定的,若要让“setImmediate”先执行,可以包在文件IO的回调里
1 | fs.readFile(_filename, () => { |