getClientRects() 和 getBoundingClientRect() 暴露的亚像素级布局差异可用于指纹识别——完全不需要触碰 Canvas。
页面上的每一个元素都有一个 JavaScript 可以读取到小数点后几位的精确位置与尺寸。让浏览器返回某个标题元素所占据的确切矩形,得到的会是类似 312.234375 这样的浮点数——这种精度远超人眼所需,却是字体微调(hinting)、亚像素抗锯齿和平台特有文本排版共同作用的产物。这些数值在不同设备上会有细微差异,只要脚本测量足够多这样的数值,就能把普通的页面排版变成一种追踪信号——全程不需要 Canvas 元素,也不需要读取任何像素。
核心要点
getClientRects()和getBoundingClientRect()返回具有亚像素(浮点数)精度的DOMRect值——这一精度会随字体渲染、DPI 缩放以及操作系统文本排版引擎的不同而变化。- 测量渲染文本的矩形是一种基于布局的指纹技术:不需要
<canvas>元素,不需要读取像素,也不需要任何权限提示,因此能绕过专门针对 Canvas 的防护措施。 - 它与 Canvas 指纹相关但并不相同,二者都源自字体与渲染管线的差异;将两者结合能获得比单独使用任何一个都更高的置信度。
- GoLogin、Dolphin{anty} 等指纹浏览器都内置了专门的 "Rects" 伪装开关——这从侧面证明,业界确实在实际检测中使用这一信号。
- BrowserInsight 的指纹检测可以让你同时看到浏览器当前暴露的布局信号与 Canvas 信号。
getClientRects() 到底返回了什么
getClientRects() 及其更简单的对应方法 getBoundingClientRect() 都是普通的布局 API,由 CSSOM View 规范 标准化。它们的设计初衷,是让 JavaScript 能够回答诸如"这个元素当前是否在视口内可见"或"这行换行文本具体在哪里结束"这类日常问题,看起来完全不像追踪工具。
对任意元素调用 getBoundingClientRect(),会得到一个 DOMRect——包含 top、left、width、height 以及由此推导出的边界值——描述它相对于视口的包围盒。而调用 getClientRects() 则会返回一整份矩形列表,每个行盒(line box)对应一个矩形,这对内联内容尤为重要:一句会换行成三行的文字,就会返回三个各自拥有独立坐标的矩形。Range.getClientRects() 的作用与之相同,只不过作用于任意一段文本选区,让脚本可以测量特定的一段字符,而不是整个元素。
返回的数值并非整数,而是双精度浮点数,因为浏览器的排版引擎在定位文本时使用的是亚像素级运算,尽管你的屏幕最终会把一切都四舍五入到物理像素。这份额外的精度——那部分永远不会以完全相同的方式渲染在不同设备上的小数余数——正是这种指纹技术的全部依据。
为什么数值会因设备而异
如果两个浏览器用相同的 HTML 和 CSS 渲染同一段内容,为什么矩形数值还会不同?在"这里有一段文字"到"这是它最终的像素"之间,还有好几层因素各自引入了细微而可复现的差异:
- 字体微调(hinting)与整形(shaping):操作系统的文本整形引擎——Windows 上的 DirectWrite、macOS 上的 Core Text、Linux 上的 FreeType/HarfBuzz——决定了字形轮廓究竟如何对齐到像素网格。不同的 hinting 规则会为同一字体中的同一字符串产生不同的前进宽度(advance width)。
- 设备像素比:高分屏(
window.devicePixelRatio为 2 或 3)会将亚像素布局四舍五入到比 1x 屏幕更精细的物理网格上,从而改变每个坐标的小数部分。可参考 MDN 的devicePixelRatio文档了解浏览器如何暴露这一信息。 - 字体可用性与回退:如果请求的字体未安装,浏览器会替换为度量值不同的回退字体——这与字体指纹所依赖的机制相同,只不过此处的信号是原始矩形几何数据,而不是简单的"存在/不存在"位。
- 浏览器引擎与版本:Chromium(Blink)、Firefox(Gecko)和 Safari(WebKit)各自实现了自己的文本排版与换行逻辑,因此完全相同的标记在不同引擎之间、有时甚至在同一引擎的不同版本之间,测量结果就可能相差几百分之一像素。
- 缩放级别与操作系统文本缩放:页面缩放和操作系统级的辅助功能缩放(Windows 的"放大文本"、macOS 的显示缩放)都会进入同一个排版管线,并改变亚像素余数。
这些都不是任何人为了隐私而主动设置的选项,而是正常渲染过程的副作用——这正是为什么最终得到的矩形对同一设备、同一会话而言是稳定的,但在不同设备之间却会产生差异。
ClientRects 指纹 vs Canvas 指纹
这两种技术是近亲——都利用了根植于字体、GPU 和操作系统文本引擎的渲染管线差异——但在触碰的对象以及被发现的难易程度上有所不同。
| 属性 | ClientRects 指纹 | Canvas 指纹 |
|---|---|---|
| API 接口 | 普通 DOM 布局(getClientRects、getBoundingClientRect) | <canvas> + 2D 上下文(toDataURL、getImageData) |
| 是否需要读取像素 | 否 | 是 |
| 是否对 Canvas 专用拦截工具可见 | 否 | 是——扩展程序会挂钩这些具体调用 |
| 信号来源 | 亚像素级文本/元素布局几何数据 | 光栅化像素颜色 |
| 典型熵值 | 单独使用时中等;与字体结合时较强 | 单独使用时较高 |
| 可被检测为"读取了一个离屏 Canvas" | 不适用——完全不涉及 Canvas | 是,可通过监控 API 调用检测 |
这带来的实际后果,正是 ClientRects 指纹对追踪者和指纹浏览器厂商都颇具吸引力的原因:任何专门监视 Canvas API 调用的防护措施——无论是 Canvas 拦截扩展、farbling 层,还是我们 Canvas 指南中提到的那种监控脚本——在这里都无从下手,因为压根不涉及任何 Canvas。脚本只需悄悄测量页面上已有的普通文本和标题的布局,就能得到一个可比拟的信号。
实际测量是如何进行的
一个典型的实现会渲染若干带样式的文本元素——通常混用多种字体、字号,以及已知会触发不同亚像素舍入行为的 CSS 属性(比如 letter-spacing)——然后读回它们的矩形:
// ClientRects 测量的示意实现
function measureRects() {
const probe = document.createElement('div');
probe.style.cssText = 'position:absolute; visibility:hidden; font-size:14px; letter-spacing:0.3px;';
probe.style.fontFamily = 'Arial, sans-serif';
probe.textContent = 'BrowserInsight fingerprint probe 0123456789';
document.body.appendChild(probe);
const rects = probe.getClientRects();
const values = Array.from(rects).flatMap(r => [r.width, r.height, r.left, r.top]);
document.body.removeChild(probe);
return values; // 与其他信号一同送入哈希函数
}
这个元素不需要真正可见——visibility: hidden 的元素依然会参与布局计算,因此它产生的矩形是真实的。通常会测量并哈希多个探测字符串、字体和 CSS 组合,这与其他指纹技术普遍采用的"测量许多个小信号、再组合起来"的思路一致——具体这些信号如何叠加成一个完整的标识符,可参考我们的浏览器指纹技术完全指南。
这项技术实际出现在哪里
相比 Canvas 或 WebGL,这项技术在主流报道中并不常见,但在生态系统的两个特定角落,它确实是已知的存在:
- 指纹识别/反欺诈厂商会把基于矩形的测量作为众多输入信号之一,原因很简单:计算成本低,且与大多数隐私工具关注的防护方向正交。
- 指纹浏览器——这类工具专门为每个浏览器实例呈现不同却一致的伪装指纹——对它相当重视,甚至专门提供了配套控制项。GoLogin 和 Dolphin{anty} 都在 Canvas、WebGL 开关旁边,专门为伪装或规范化 "Rects" 输出提供了设置。厂商愿意专门为某个信号构建伪装控制项,本身就是"这个信号确实被检测"的一个合理佐证。
如何降低你的暴露程度
主流浏览器中并没有一个专门的"禁用 getClientRects"开关,因为这个 API 对普通布局功能不可或缺——文本选择、滚动定位、工具提示,以及几乎所有 JavaScript UI 框架都依赖它。可用的缓解措施与对付 Canvas 指纹的方法大同小异,因为二者的根源相同:
- 优先选择"趋同"而非"定制"。 Tor Browser 会标准化字体渲染,并降低若干精细布局 API 的精度,从而缩小你在其中可被区分出来的群体范围——这与它对待 Canvas 的策略如出一辙。
- 限制已安装字体和扩展程序的数量。 你安装的每一种额外字体,都为回退替换机制改变矩形尺寸多提供了一种可能。使用系统自带字体集合的标准安装,比经过大量个性化定制的系统更难被单独识别出来。
- 使用类似 resistFingerprinting 的保护机制。 Firefox 的
privacy.resistFingerprinting会对多项渲染相关数值进行取整和标准化,从而降低这项技术所依赖的亚像素方差——这与它约束 Canvas 和 WebGL 输出的方式相同。 - 检查自己暴露了什么。 运行 BrowserInsight 的指纹检测,一次性查看你的 Canvas、WebGL 及其他渲染信号,然后对比普通浏览器与经过隐私强化的浏览器的检测结果,看看哪些设置真正起到了作用。
与 Canvas 指纹一样,"反指纹悖论"同样适用:一个过度矫正、手工打造的 Rects 伪装——比如返回可疑的整数,或者在每次探测中都呈现零方差的亚像素结果——反而可能比它试图隐藏的原始信号更加显眼。
常见问题
ClientRects 指纹和 Canvas 指纹是一回事吗?
不是,但二者密切相关。Canvas 指纹从离屏 <canvas> 中读回光栅化的像素;ClientRects 指纹读取的是普通 DOM 元素或文本的亚像素级布局几何数据,完全不涉及 Canvas。二者都可以追溯到字体渲染与平台文本排版的差异,因此往往存在相关性,但一个只针对 Canvas 设计的防护措施,对这项技术没有任何阻挡作用。
Canvas 拦截扩展能挡住这项技术吗?
不能,单靠它本身不行。Canvas 拦截器挂钩的是 toDataURL()、getImageData() 这类 Canvas 专用 API。getClientRects() 和 getBoundingClientRect() 是完全不相关的 DOM 布局方法,因此专门针对 Canvas 的拦截器没有理由去拦截它们。
这项技术单独就能识别出我吗?
单独使用时通常置信度不高——很多设备的渲染管线相似,会产生相近的矩形结果。它对追踪者更大的价值在于与字体、Canvas、WebGL 等其他被动属性结合使用,这与完整指纹中每一个独立信号只贡献部分熵值、而非完整身份的规律一致。
为什么指纹浏览器会专门提到 "Rects" 设置?
因为这项技术确实存在,并被部分指纹识别和反欺诈厂商实际检测。那些致力于呈现一致伪装身份的工具,必须像处理 Canvas 和 WebGL 那样,对它进行规范化或随机化——否则即便其他方面伪装得再好,Rects 信号也会泄露出真实的底层设备信息。
结语
ClientRects 指纹提醒我们,"防指纹"不能简单地等同于"屏蔽 Canvas 就万事大吉"。任何精细到足以反映字体渲染、DPI 缩放或平台文本排版的 API,都精细到足以被用于指纹识别——而每个网页应用都已经在依赖的普通 DOM 布局方法正是如此。理解其原理,才能让你判断某个隐私工具是否真的覆盖了这一层面,而不是想当然地认为 Canvas 防护就是全部答案。
推荐阅读:


