试问哪个前端开发不想深入了解浏览器呢,想了解它的过去,彼此互动的细节,日日陪伴的点点滴滴...🙂↔️
VSync
VSync,全称为Vertical Synchronization(垂直同步),是一种图形技术,用于将游戏或应用程序的帧率与显示器的刷新率同步,从而消除画面撕裂(Screen Tearing)现象。 画面撕裂是指显卡输出帧率与显示器刷新率不匹配时,屏幕上同时显示多个帧的部分内容,导致图像水平分裂。其核心是通过等待显示器的垂直空白间隔(VBlank)信号,将 GPU 的帧渲染输出与显示器的刷新率精确对齐,从而消除屏幕撕裂(Screen Tearing)。
基础概念
- 刷新率(Refresh Rate):显示器每秒刷新屏幕的次数,例如 60Hz 表示每 16.67ms 刷新一帧。
- 帧率(Frame Rate):GPU 每秒渲染的帧数(FPS)。
- VBlank(垂直空白间隔):显示器完成一帧扫描(从上到下逐行绘制)后,短暂的“空白期”(约 1-2ms),用于重置扫描位置。在此期间安全交换帧数据。
- 屏幕撕裂原因:无 VSync 时,GPU 在显示器扫描中途提交新帧,导致屏幕同时显示旧帧上半部和新帧下半部,形成水平断裂线。
显示器刷新原理
显示器(无论 CRT、LCD 或 OLED)刷新一帧的原理类似:逐行扫描。
- 从屏幕顶部开始,一行一行绘制像素(Active Scan 期,约 15ms)。
- 扫描完底部后,进入 VBlank 期(约 1.67ms),电子束/扫描器返回顶部,准备下一帧。
- 显示器在 VBlank 开始时发送 VSync 信号给 GPU(通过 HDMI/DisplayPort 等接口)。
- 如果 FPS > 刷新率:GPU 渲染过快,会在 VBlank 后闲置等待(帧率上限锁定)。
- 如果 FPS < 刷新率:可能跳帧(显示重复旧帧)或卡顿(等待累积)。
与浏览器的关系
VSync(垂直同步)与浏览器密切相关,主要体现在浏览器渲染流水线中:浏览器使用 VSync 信号同步网页帧渲染与显示器刷新率(通常 60Hz,每 16.67ms 一帧),确保动画、滚动和视频播放平滑无撕裂(screen tearing)。 这类似于游戏中的 VSync,但浏览器优化为合成线程(Compositor)主导,减少主线程(JS/布局)阻塞导致的卡顿(jank)
VSync 信号的来源(Source)
VSync 并非 Chrome 自己产生,而是来自操作系统或硬件:
| 平台 | VSync 来源 |
|---|
| Android | SurfaceFlinger + HWC(Hardware Composer)通过 DispSync 软件 PLL 生成 1 |
| Linux | DRM/KMS 子系统(Direct Rendering Manager / Kernel Mode Setting)提供vblank 事件 |
| Windows | DirectX 的IDXGIOutput::WaitForVBlank() 或 DWM(Desktop Window Manager)合成器 |
| macOS | Core Video 的CVDisplayLink,与显示器刷新率同步 |
| ChromeOS | 基于 Linux DRM,但通过Ozone 抽象层接入 |
INFO
Chrome 通过 平台抽象层(如 ui/gfx/overlay、ui/compositor)统一这些差异
VSync 的捕获:VSyncProvider
Chrome 定义了 VSyncProvider 接口(//ui/compositor/vsync_provider.h),各平台实现具体逻辑:
- Android:
AndroidVSyncProvider
- 注册
Choreographer.FrameCallback
- 通过 JNI 调用 Java 层
Choreographer.postFrameCallback
- Linux:
DrmVSyncProvider 或 WaylandVSyncProvider
- 监听 DRM 的
DRM_EVENT_VBLANK 或 Wayland 的 frame 事件
- macOS:
DisplayLinkMac
- 使用
CVDisplayLinkSetOutputCallback
当 VSync 到来时,这些 Provider 会调用:
vsync_observer_->OnVSync(vsync_time, interval);
VSync 的分发:Viz Display Compositor
这是 Chrome 图形栈的核心——Viz(Visuals)系统,负责帧调度和合成。
关键组件:
DisplayScheduler(//components/viz/service/display/display_scheduler.cc)
- 接收
OnVSync
- 触发
BeginFrame
- 管理帧节奏(是否节流、是否跳过)。
BeginFrameSource
- 将 VSync 转换为
BeginFrameArgs(含帧 ID、时间戳、间隔);
- 广播给所有订阅者(如 Renderer 的合成线程)。
[ Hardware VSync ]
↓
VSyncProvider::OnVSync()
↓
viz::DisplayScheduler::OnBeginFrameSourceNeedsBeginFrames()
↓
Generate BeginFrameArgs (frame_id, deadline, interval)
↓
Send via IPC to Renderer Process → Compositor Thread
INFO
此过程通过 Mojo IPC(Chrome 的现代 IPC 系统)完成,高效且类型安全。
VSync 在 Renderer 进程中的使用
合成线程(Compositor Thread)
- 收到
BeginFrame 后:
- 更新合成属性动画(
transform, opacity)
- 检查是否需要请求主线程更新(
needs_main_frame = true)
- 若不需要,则直接合成并提交帧。
主线程(Main Thread)
终于到了喜闻乐见的主线程环节,下面的东西相对于前端将会相对熟悉些。
- 若合成线程标记
needs_main_frame,则:
- 执行
requestAnimationFrame 回调;
- 运行微任务;
- 执行 Style/Layout/Paint;
- 提交
LayerTree 给合成线程。
INFO
主线程的整个渲染流水线是由合成线程“按需拉取”的,而非主动推送。
Task Queue(任务队列)
HTML Living Standard 定义的 window event loop(主线程事件循环)核心是:
- Task Queue:不是一个队列,而是每个 Task Source 一个独立的 queue(规范允许 UA 按优先级挑选)。 常见 Task Source(优先级由浏览器决定):
- User interaction(click、keydown、scroll、pointer events)——最高优先
- Rendering task source(专用于渲染)
- Timer(setTimeout/setInterval)
- Networking(fetch/XHR)
- DOM manipulation、History 等
- Microtask Queue:全局只有一个。来源:Promise.then/await、queueMicrotask()、MutationObserver、process.nextTick(Node)。
- Microtask Checkpoint:每次宏任务执行完后立即执行,且一次性清空整个队列(即使中间又 enqueue 新 microtask)。
- Rendering Opportunity:浏览器在并行监控(不占用主线程),当满足刷新率(通常 60Hz ≈16.7ms)、页面可见、DOM 有变化时,queue 一个 global task 到 Rendering Task Source,触发 "Update the rendering"。
可见任务队列有多个,是根据任务来源对任务做了分类的结果。用户的输入事件具有最高优先级,这意味着相关代码会最先执行。
可以用下列伪代码来表示上述流程:
1. Call Stack 为空
↓
2. 执行 Microtask Checkpoint(清空所有微任务,直到队列为空)
- Promise.then / await
- queueMicrotask
- MutationObserver
↓(这一步可能持续几毫秒)
3. 浏览器判断是否需要渲染(Rendering Opportunity?)
- 是 → 选中 Rendering Task Source 的 task
执行:
• rAF 回调(带高精度时间戳)
• Style Recalc
• Layout
• Paint
• Commit to Compositor
- 否 → 跳过
4. 挑选下一个 Macrotask(从所有 Task Queue 挑,Chrome 优先级:Input > Animation > Default > Idle)
- 执行该宏任务(可能又产生新微任务)
- 执行完后再次 Microtask Checkpoint
5. 循环回到第 1 步
Chrome 把主线程分成多个阶段,Performance 面板里你能看到精确的黄色块(Main Thread):
一次典型帧(Frame)的流程(~16.7ms 内必须完成):
- BeginFrame(VSync 信号到来)
- Input 处理(如果有用户事件,优先执行)
- JS 执行(当前宏任务 + 所有微任务)
- rAF 回调阶段(如果这是 Rendering Task)
- Style Recalculation
- Layout(Reflow / Recalculate Layout)
- Paint(Record paint commands,不真正画像素)
- Commit(IPC 发给 Compositor Thread)
- Compositor Thread:Layer 合成、GPU Raster(真正画到屏幕)

你可以通过下面一段代码来验证流程:
console.log('1. sync start');
setTimeout(() => console.log('5. setTimeout'), 0);
Promise.resolve().then(() => console.log('3. Promise'));
requestAnimationFrame(() => console.log('4. rAF'));
queueMicrotask(() => console.log('2. microtask'));
console.log('sync end');