帧率指纹:刷新率追踪
requestAnimationFrame 时序和显示器刷新率如何创建指纹信号, 以及在引擎级别控制帧率的技术。
简介
requestAnimationFrame API 的设计初衷是帮助开发者通过将绘图操作与显示器的刷新周期同步来创建流畅的动画。与使用 setTimeout 等任意定时器不同,requestAnimationFrame 在下一次屏幕重绘之前调用函数,通常以显示器的原生刷新率运行。这带来了更流畅的动画、更好的电池效率和更灵敏的用户体验。
然而,requestAnimationFrame 的回调频率直接暴露了显示器的刷新率。标准的 60 Hz 显示器大约每 16.67 毫秒触发一次回调,而 144 Hz 的游戏显示器每 6.94 毫秒触发一次。120 Hz 显示器、240 Hz 显示器和可变刷新率 (VRR) 显示器各自产生可测量的不同回调间隔。这些信息无需任何权限即可被任何网站访问,使其成为可靠且持久的指纹信号。
隐私影响
显示器刷新率是一种用户在隐私语境下很少考虑的硬件特征。与 Cookie 或 IP 地址不同,刷新率与物理显示器硬件绑定,不会在浏览器会话之间、隐私浏览模式中或清除网站数据后发生变化。
随着显示技术的多样化,指纹识别风险已显著增加。十年前,几乎所有消费级显示器都运行在 60 Hz,提供的区分信息很少。如今,市场上包括 60 Hz、75 Hz、90 Hz、120 Hz、144 Hz、165 Hz、240 Hz 和 360 Hz 面板。多显示器配置可能根据浏览器窗口所在的显示器报告不同的刷新率。具有动态刷新率功能的笔记本电脑(如 Apple 的 ProMotion,120 Hz)增加了更多变化。
这种多样性意味着帧率信息现在携带有意义的熵。使用 165 Hz 显示器的访客比使用 60 Hz 显示器的访客稀有得多,使刷新率成为缩小身份范围的有价值信号。当与屏幕分辨率、色深、GPU 信息和其他显示相关信号结合时,帧率会构成详细的硬件特征。
问题因多个 API 泄漏此信息而加剧。除了 requestAnimationFrame 时序外,CSS 动画、Screen API,甚至 setTimeout 回调的时序(浏览器将其与显示周期对齐)都可以揭示刷新率。
技术背景
requestAnimationFrame 如何暴露刷新率
当网站调用 requestAnimationFrame(callback) 时,浏览器在每次屏幕重绘之前调用回调函数。回调接收一个 DOMHighResTimeStamp 参数,指示帧开始的时间。通过测量连续时间戳之间的间隔,网站可以高精度地确定显示器的刷新率。
测量过程很直接:收集 30 到 60 个帧时间戳,计算平均间隔,然后推导出频率。只需 10 帧就可以可靠地确定刷新率。在 60 Hz 显示器上,测量过程不到 200 毫秒,且对用户不可见。
CSS 动画和过渡
CSS 动画默认以显示器的刷新率运行。一个恰好持续一秒的 @keyframes 动画在 60 Hz 显示器上会产生 60 个中间帧,在 144 Hz 显示器上会产生 144 帧。虽然 CSS 规范不保证逐帧精确,但动画事件的时序(animationstart、animationend、过渡时序)仍然会泄漏刷新率信息。
可变刷新率的复杂性
具有可变刷新率技术(G-Sync、FreeSync、ProMotion)的现代显示器可以根据内容动态改变刷新率。这创造了一个额外的指纹维度:不仅是基础刷新率,自适应行为模式本身也成为信号。一个根据内容活动在 48 Hz 和 120 Hz 之间切换的显示器,其时序模式与固定刷新率显示器不同。
多显示器配置
当浏览器窗口跨越多个显示器或在不同刷新率的显示器之间移动时,requestAnimationFrame 时序会相应变化。这种过渡模式可以揭示多显示器配置,而多显示器配置本身就是一个指纹信号。
常见保护方法及其局限性
VPN 和代理服务器
VPN 对显示器刷新率没有任何影响。帧率是完全在浏览器渲染管线内测量的本地硬件属性。网络层面的隐私工具无法修改它。
隐身和隐私浏览
隐私浏览模式不会改变显示器的刷新率或 requestAnimationFrame 的行为。隐身模式下的帧率指纹与普通窗口中的指纹完全相同。
浏览器扩展
试图修改 requestAnimationFrame 行为的扩展面临重大挑战:
- 包装回调: 扩展可以拦截
requestAnimationFrame并将回调限速到目标速率(例如 60 FPS)。但这会在合法动画中产生明显卡顿,并且可以通过比较requestAnimationFrame时序与 CSS 动画时序来检测,而扩展可能无法控制后者。 - 修改时间戳: 修改传递给回调的
DOMHighResTimeStamp可以模拟不同的刷新率,但与performance.now()、Date.now()和 CSS 过渡时序的不一致会暴露修改。 - 阻止 API: 完全禁用
requestAnimationFrame会破坏大多数依赖它进行渲染的现代 Web 应用。
根本问题在于扩展运行在 JavaScript API 层,无法控制实际的渲染管线。显示器的真实刷新率同时影响多个浏览器子系统,修改一个 API 而不控制所有 API 会产生可检测的不一致。
浏览器级别限速
一些浏览器在开发者设置中提供帧率限制。这种方法限制了渲染速率,但不提供精细控制,无法按会话设置,而且通常会引入表明限速的视觉伪影。
BotBrowser 的引擎级方案
BotBrowser 通过 --bot-fps 标志在浏览器引擎的渲染管线级别控制帧率。这不是 JavaScript 包装器或 API 拦截器。渲染循环本身以指定的速率运行,确保所有帧率相关的信号在内部一致。
基于配置文件的帧率
使用 profile 模式时,BotBrowser 从加载的指纹配置文件中读取目标帧率:
chrome --bot-profile="/path/to/profile.enc" \
--bot-fps=profile \
--user-data-dir="$(mktemp -d)"
配置文件根据目标设备定义显示器的预期刷新率。代表标准办公笔记本的配置文件报告 60 FPS。游戏台式机配置文件可能报告 144 FPS。渲染管线以此速率运行,因此 requestAnimationFrame 回调、CSS 动画和所有其他帧依赖的时序都与配置文件的显示特征一致。
自定义帧率
对于特定用例,你可以设置精确的帧率:
chrome --bot-profile="/path/to/profile.enc" \
--bot-fps=60
这将强制渲染管线以 60 FPS 运行,无论主机显示器的实际刷新率如何。所有时序相关的测量将报告大约每帧 16.67 毫秒。
真实显示率
当你希望浏览器使用实际显示器的刷新率时:
chrome --bot-profile="/path/to/profile.enc" \
--bot-fps=real
引擎级别一致性
由于控制应用在渲染管线级别,所有帧率相关的信号都是一致的:
requestAnimationFrame回调以受控速率触发- 帧时间戳匹配预期间隔
- CSS 动画以正确的节奏推进
- 动画帧期间的
performance.now()时序与报告的速率一致 - JavaScript 测量的帧率与实际渲染行为之间没有差异
配置与使用
基本 CLI 使用
# 使用配置文件定义的帧率
chrome --bot-profile="/path/to/profile.enc" \
--bot-fps=profile \
--user-data-dir="$(mktemp -d)"
# 固定 60 FPS
chrome --bot-profile="/path/to/profile.enc" \
--bot-fps=60
# 实际显示率
chrome --bot-profile="/path/to/profile.enc" \
--bot-fps=real
Playwright 集成
const { chromium } = require('playwright-core');
(async () => {
const browser = await chromium.launch({
executablePath: '/path/to/botbrowser/chrome',
args: [
'--bot-profile=/path/to/profile.enc',
'--bot-fps=60',
],
headless: true,
});
const context = await browser.newContext({ viewport: null });
const page = await context.newPage();
await page.goto('https://example.com');
// requestAnimationFrame will fire at 60 FPS
await browser.close();
})();
Puppeteer 集成
const puppeteer = require('puppeteer-core');
(async () => {
const browser = await puppeteer.launch({
executablePath: '/path/to/botbrowser/chrome',
args: [
'--bot-profile=/path/to/profile.enc',
'--bot-fps=profile',
],
headless: true,
defaultViewport: null,
});
const page = await browser.newPage();
await page.goto('https://example.com');
await browser.close();
})();
结合时序保护
要完整控制硬件信号,可以将帧率控制与时序缩放结合使用:
chrome --bot-profile="/path/to/profile.enc" \
--bot-fps=60 \
--bot-time-scale=1.0 \
--bot-noise-seed=42 \
--user-data-dir="$(mktemp -d)"
验证
使用配置的帧率启动 BotBrowser 后,验证结果:
const measuredFps = await page.evaluate(() => {
return new Promise(resolve => {
const timestamps = [];
function frame(ts) {
timestamps.push(ts);
if (timestamps.length < 61) {
requestAnimationFrame(frame);
} else {
const intervals = [];
for (let i = 1; i < timestamps.length; i++) {
intervals.push(timestamps[i] - timestamps[i - 1]);
}
const avgInterval = intervals.reduce((a, b) => a + b) / intervals.length;
resolve({
fps: Math.round(1000 / avgInterval),
avgInterval: avgInterval.toFixed(2),
minInterval: Math.min(...intervals).toFixed(2),
maxInterval: Math.max(...intervals).toFixed(2),
});
}
}
requestAnimationFrame(frame);
});
});
console.log('Measured FPS:', measuredFps.fps);
console.log('Average interval:', measuredFps.avgInterval, 'ms');
检查要点:
- 测量的 FPS 与配置值匹配(例如
--bot-fps=60对应 60) - 帧间隔与目标速率一致
- 多种测量方法(requestAnimationFrame、CSS 动画时序)产生相同结果
- 指纹测试工具报告预期的刷新率
最佳实践
-
使 FPS 与配置文件硬件匹配。 代表标准办公笔记本的配置文件应使用 60 FPS。游戏台式机配置文件可以使用 120 或 144。不匹配的值会削弱指纹一致性。
-
在自动化工作流中使用
--bot-fps=profile。 这会自动从配置文件中选择正确的帧率,减少配置错误。 -
与
--bot-time-scale结合使用。 帧率和执行时序应该对齐。具有 60 FPS 帧率的慢设备配置文件应具有与入门级硬件匹配的时序特征。 -
避免任意 FPS 值。 选择与真实显示硬件对应的值:24、30、48、60、75、90、120、144、165、240。像 73 或 137 这样的值不对应任何真实显示器,会显得突兀。
-
在 headless 模式下测试。 Headless 浏览器没有物理显示器,因此帧率完全由 BotBrowser 的配置决定。验证配置的 FPS 在 headless 模式下正确报告。
常见问题
帧率指纹在移动设备上有效吗?
有效。移动设备有不同的刷新率(60 Hz、90 Hz、120 Hz),许多现代手机支持动态刷新率切换。requestAnimationFrame 时序在移动浏览器上与桌面浏览器上一样可以揭示当前刷新率。
网站能检测到 FPS 正在被控制吗?
如果控制应用在 JavaScript 层面(包装 requestAnimationFrame),可以,因为 CSS 动画和其他时序来源可能报告不同的速率。BotBrowser 在渲染管线级别控制 FPS,因此所有时序来源是一致的。
--bot-fps 会影响视频播放吗?
视频播放使用独立的解码管线,不受 --bot-fps 标志影响。视频以其编码帧率播放。只有渲染循环和动画时序受到控制。
可变刷新率内容会怎样?
BotBrowser 根据配置应用固定帧率。这与大多数真实浏览器在使用固定刷新率显示器时的行为一致,而固定刷新率显示器仍然是大多数。
可以为不同标签页使用不同的 FPS 值吗?
--bot-fps 标志应用于整个浏览器实例。要使用不同的帧率,需要启动具有不同配置的独立浏览器实例。
帧率如何与 visibility API 交互?
浏览器通常会对后台标签页限制 requestAnimationFrame。BotBrowser 遵循此行为,除非使用了 --bot-always-active,该标志使所有标签页无论可见性状态如何都以配置的帧率运行。
Screen.refreshRate 属性匹配吗?
在暴露 screen.refreshRate 或类似属性的浏览器上,BotBrowser 确保这些值与 --bot-fps 配置的帧率和加载的配置文件一致。
总结
显示器刷新率可通过 requestAnimationFrame 时序、CSS 动画和相关 API 进行测量,是标准隐私工具无法解决的持久性硬件指纹信号。BotBrowser 通过 --bot-fps 标志在浏览器引擎的渲染管线级别控制帧率,确保所有帧依赖的信号在内部一致。结合 性能时序保护、Canvas 控制 和 全面的配置文件管理,BotBrowser 为注重隐私的工作流提供完整的硬件信号保护。