解析网页如何推测 DevTools 已打开:窗口尺寸差、debugger 语句计时与控制台 getter,各自为何误判,以及为何只是弱信号。
有些网页在你按下 F12 的瞬间就变得古怪:内容被清空,标签页卡死,或者弹出一条怎么也关不掉的“调试器已暂停”提示。这并不神秘:页面上的脚本正在猜测 DevTools 是否已打开,依据是浏览器无法对页面 JavaScript 隐藏的副作用。本文介绍三种经典手法、各自的失效方式,以及为什么它们单独都算不上可靠信号。
核心要点
- 没有任何浏览器会告诉页面“DevTools 已打开”。 所有检测都是启发式推断,依据是窗口尺寸变化、脚本被暂停,或某次控制台调用的表现不同。
- 窗口尺寸差比较
outerWidth与innerWidth(以及高度)。停靠式 DevTools 会占用视口空间,差值变大;独立窗口式 DevTools 则不改变差值。 debugger语句只在 DevTools 打开时才会暂停执行,因此脚本可以给暂停计时,或循环触发该语句来“惩罚”打开的面板。这正是某些页面会卡住的原因。- 控制台 getter 会在控制台格式化对象时触发,与 CDP 序列化泄露同属一类技巧,行为随浏览器版本而变。
- 这是弱信号。 开发者、客服人员和好奇的用户都会正当地打开 DevTools,缩放或侧边栏也会造成误报,所以严肃的系统只把它当作权重很低的输入。
网站为什么要找开发者工具
目的几乎从来不是抓住阅读页面的人。常见动机有:
- 反爬虫。 查看网络请求或 DOM 往往是爬取的第一步,网站可能因此提高风险评分。
- 保护前端代码。 付费墙逻辑、混淆脚本和广告验证代码,挂上调试器后更容易被研究。
- 欺诈与滥用评分。 打开检查器是又一个弱输入,与机器人检测体系中更强的证据并列使用。
请注意范围:本文讨论的是人手动打开 DevTools。通过 DevTools 背后的协议来驱动浏览器的自动化框架是另一个问题,见检测 CDP 自动化和无头浏览器检测。
手法一:窗口尺寸差
浏览器暴露两个尺寸。window.outerWidth 是包含工具栏和侧边栏在内的整个浏览器窗口宽度,window.innerWidth 则是页面视口宽度。正常情况下,宽度只差几像素的窗口边框,高度则差出标签栏和工具栏的高度,所以检测脚本会把阈值设得明显高于这个数。把 DevTools 停靠到侧边或底部后,视口缩小而外层窗口不变,差值会跳升数百像素。
const threshold = 160; // px
const widthGap = window.outerWidth - window.innerWidth > threshold;
const heightGap = window.outerHeight - window.innerHeight > threshold;
const looksOpen = widthGap || heightGap;
它开销小、完全被动、无需任何权限,所以是最常见的检查。它也会以可预见的方式失效:
- 独立窗口式 DevTools 在自己的窗口中打开,页面视口不变,因此检测不到。
- 浏览器侧边栏(阅读列表、AI 助手、垂直标签栏)同样会缩小视口,看起来像停靠的检查器。
- 页面缩放和系统显示缩放在某些平台上会改变两个值之间的关系,没有 DevTools 也会出现差值。
- 移动端和嵌入式视图常常报告奇怪或相等的数值,这项检查在那里没有意义。
这两个值同样会用于屏幕分辨率指纹中的屏幕与窗口检查。
手法二:debugger 语句
debugger 语句会调用当前可用的调试功能,如果没有则什么也不做。DevTools 关闭时它是耗时微秒的空操作;DevTools 打开(且断点生效)时,执行会停在那一行,直到用户点击继续。
这给了页面两种用法。第一种是计时:取一个时间戳,运行 debugger,再取一个,如果暂停超过几十毫秒,就认定“有调试器附加”。
const t0 = performance.now();
debugger;
const paused = performance.now() - t0 > 100; // human-scale pause
第二种是陷阱:在定时器或循环中反复调用 debugger,每次你点继续它都会再次暂停。这就是 DevTools 打开时页面看起来卡死,或不断弹出 Sources 面板的原因。浏览器允许你停用断点或跳过某个脚本,所以这只是烦人而不是屏障,而只想看看控制台的用户反而得到了更差的页面。
失效方式:标签页暂停也可能是笔记本休眠、设备负载过高或后台标签页被节流,这些都会拉长计时;停用断点后,该语句又成了空操作。
手法三:控制台 getter
DevTools 打开时,输出一个对象会让控制台为它构建预览。页面可以输出一个带 getter 属性的对象,看 getter 是否被执行。
let opened = false;
const probe = new Error();
Object.defineProperty(probe, "stack", {
get() { opened = true; return ""; }
});
console.log(probe);
getter 是否触发取决于浏览器、版本以及当前显示的控制台面板,引擎也会不时改变这一行为,所以去年有效的检查今年可能毫无反应。相关的 Chromium 机制(附加的自动化客户端会让每次 console.* 调用序列化其参数)在 CDP 检测指南中有详细说明;这里要记住的是,getter 触发既可能意味着“有人打开了控制台”,也可能意味着“有协议客户端已附加”,页面分不清是哪种。
为什么它只是弱信号
上面每种方法都是猜测,而每种猜测都有无辜的解释。网页开发者、QA 测试人员、核实消息的记者、照着客服文章操作的用户都会打开 DevTools,屏蔽他们的网站会损失真实客户。与此同时,任何决心检查页面的人都可以把面板独立出来或停用断点,所以这些检查是启发式的,也很容易绕开。因此它们只作为权重很低的特征,与行为信号等更强的证据并用,绝不单独定论。
BrowserInsight 的检测怎么做
指纹检测页面在浏览器详情中显示 DevTools“已打开”或“已关闭”的标记。它只使用窗口尺寸差,阈值为 160 px,因此表现与上文描述完全一致:
- 停靠式 DevTools(侧边或底部)能被检测到,因为视口缩小超过 160 px。
- 独立窗口式 DevTools 检测不到。
- 较大的浏览器侧边栏即使在 DevTools 关闭时也可能触发它。
机器人检测页面里有一行“开发者工具检测”,同样基于 160 px 的尺寸差,风险等级为低。发现尺寸差后它还会跑一次控制台 getter 探测,但这次探测从不单独决定结果。另有一行“移动端模拟检测”专门识别 DevTools 的设备模拟模式:桌面平台上出现移动端 User-Agent,或浏览器窗口尺寸与上报的手机屏幕对不上。
两项检测都不使用 debugger 语句,所以 BrowserInsight 绝不会暂停你的页面。你可以先把 DevTools 停靠、再改成独立窗口,分别打开工具看标记如何变化:它展示的是页面能推断出什么,而不是任何事情的证明。
常见问题
网站真的能看到我打开了 DevTools 吗?
不能直接看到。它只能从视口尺寸变化或脚本被暂停等副作用中推断,而且推断常常出错。
为什么我打开 DevTools 页面就卡住?
通常是脚本在循环中调用 debugger,每次继续都会再次暂停。在 Sources 面板中停用断点即可停止暂停。
检测 DevTools 等于检测机器人吗?
不等于。这里说的是人打开了检查器。自动化工具留下的痕迹不同,见无头浏览器检测。


