跳到主要内容

浏览器调试

浏览器调试是利用运行时证据缩小问题范围,找到行为与预期开始分离的位置,再验证修复。Console、Sources、Network 等面板提供不同角度的证据;熟悉面板名称只是起点,关键是知道当前假设需要观察什么。

遇到“点一次执行两次”“有请求但页面白屏”或“本地正常、用户异常”,先写清操作、预期、实际结果和环境,再选择工具。下面以 Chrome DevTools 的通用功能为例,说明怎样把复现、定位与回归连起来。

1. 面试核心​

先记住这几条,再读正文
  • 先建立可重复的触发条件,再用证据逐步缩小范围。一条报错或一次刷新恢复不能直接证明根因,修复应能解释原来的现象。卡 · 复现与最小假设
  • 断点让我们观察暂停时的作用域和调用栈。条件断点、异常断点、DOM 断点和请求断点适合不同入口,不必到处插入日志。卡 · 断点、作用域与调用栈
  • Network 要联合查看请求、响应、发起者和时序。状态码成功不代表响应体符合应用协议,看到失败也不等于请求没有到达服务端。卡 · 请求、响应与发起者
  • 样式问题沿最终值与匹配规则回查。DOM 存在、规则写了、类名出现了,都不保证当前计算样式就是预期值。卡 · 从最终样式回查
  • 白屏是现象,需要区分加载、执行、状态和呈现阶段。按具体设备、资源版本和关键路径查证,不能只归结为 JavaScript 报错。卡 · 综合题
  • 性能与内存问题需要对应记录。长任务、网络等待、布局和对象保留关系属于不同原因,不能用一次截图或总内存数字代替分析。卡 · 性能、内存与存储证据
  • 线上复现不了时,先补齐版本、环境和操作证据。日志、错误栈和 Source Map 需要与同一发布版本对应,同时控制敏感信息和采集量。卡 · 线上问题与修复验证

2. 复现与最小假设​

2.1 把现象变成可检查的问题​

一份可用的复现记录至少说明:哪个页面与版本、什么初始状态、做了什么操作、预期是什么、实际是什么,以及失败是否稳定。对设备相关问题,再补充浏览器版本、视口、网络条件、账号状态和相关能力支持。

假设应该能被观察结果支持或推翻。例如“按钮重复提交”可能来自多个监听器、两条调用路径、组件重复挂载、请求重试或用户重复点击。先数实际点击、处理函数进入次数和请求次数,才能判断重复从哪一层出现。

不要同时改多个不相关条件。清缓存、关闭扩展、切网络和换代码一起做,即使恢复正常,也无法知道哪个变化起作用。调试过程需要尽量保留对照。

2.2 重复监听示例​

建立 index.html:

<button id="increment" type="button">加一</button>
<output id="counter">0</output>
<script src="./counter.js"></script>

下面的 counter.js 能执行,但存在重复注册监听器的问题,用于复现错误:

const button = document.querySelector('#increment');
const counter = document.querySelector('#counter');
const state = { count: 0 };

function render() {
counter.textContent = String(state.count);
button.addEventListener('click', function increment() {
state.count += 1;
render();
});
}
render();

第一次点击显示 1,第二次显示 3。render() 每次都会创建一个新的 increment 函数对象并注册,已有监听器没有移除;第二次点击开始时已经有两个监听器,所以计数增加两次。这不是数字渲染错误,而是处理函数执行次数不符合预期。

在 Chrome DevTools Console 中可以查询:

getEventListeners(document.querySelector('#increment')).click.length

页面刚加载时是 1,第一次点击后变成 2,第二次点击后变成 4。getEventListeners 是 DevTools 的控制台工具函数,不是页面可直接使用的标准 Web API。它展示该对象注册的监听器,框架的事件委托还可能把监听器注册在祖先等其他位置,不能据此概括全部框架事件逻辑。

3. 断点、作用域与调用栈​

3.1 选择暂停入口​

断点让执行在指定条件下暂停,以便检查当时的变量和调用关系。

已知线索合适的入口想回答的问题
已知可疑代码行行断点或条件断点到这里时值是什么,条件是否成立
知道抛异常但不知道来源异常断点异常最初在哪里产生,是否被捕获
DOM 被意外修改DOM 变更断点哪段脚本修改了节点、属性或子树
请求意外发出XHR/fetch 请求断点哪条调用链启动了请求
只知道某种交互触发事件监听器断点事件进入了哪个处理函数

在前例 state.count += 1 上设置条件 state.count === 1,第二次点击时可以暂停在满足条件的那次执行。检查 Scope 中的变量与 Call Stack,能把当前状态和调用路径对应起来;继续执行后再观察是否进入其他监听器。

如果只知道 output 被改动,可以在 Elements 为它设置子树变更断点,再点击按钮。浏览器会在相关修改发生时暂停,从调用栈追到 render()。这比全局搜索所有可能写 DOM 的代码更有针对性。

3.2 单步与表达式的边界​

Step over 执行当前调用而不进入它,Step into 尝试进入调用,Step out 运行到当前调用返回。异步调度会形成新的调用阶段,异步调用栈可以帮助关联调度位置,但不代表所有历史状态都被完整保存。

暂停时的表达式在当前运行环境中求值,读取 getter 或主动调用函数可能有副作用。条件断点和 Watch 应尽量使用简单、无副作用的表达式,不要让调试动作本身改变问题。

暂停还会改变时序。某个竞态在单步时消失,只能说明调试影响了执行条件;可以改用有针对性的日志、Logpoint 或性能记录观察自然运行,再通过可控的延迟与输入顺序复现。

3.3 用同一操作验证修复​

修复应把监听器注册与视图更新分开。将 counter.js 替换为:

const button = document.querySelector('#increment');
const counter = document.querySelector('#counter');
const state = { count: 0 };

function render() {
counter.textContent = String(state.count);
}
function increment() {
state.count += 1;
render();
}
button.addEventListener('click', increment);
render();

现在连续点击两次得到 1、2,监听器数量保持 1。只在事件处理中添加一个节流函数,会改变触发条件,却没有解释和消除重复注册的原因。

如果这段代码属于可挂载、卸载的组件,还要在销毁时用同一个 increment 引用移除监听器,或用组件生命周期管理它。修复验证应包括原复现操作、重复挂载或清理条件,以及它可能影响的相关行为。

4. 网络请求与资源版本​

4.1 请求、响应与发起者​

Network 记录把一次请求的地址、方法、头、状态、响应体与时序关联起来。排查时先确定观察的是哪一次请求,而不是只看列表中是否有同名地址。

例如下面的函数期待一个 JSON 对象:

async function loadProfile() {
const response = await fetch('/api/profile');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const profile = await response.json();
if (!profile || typeof profile.displayName !== 'string') {
throw new Error('Invalid profile');
}
return profile.displayName;
}

若服务端错误地返回 HTML 页面并附带 200,请求在 Network 中可能显示成功,但 response.json() 仍会失败。应检查最终地址、Content-Type 与实际响应体,判断是否被登录跳转、代理规则或单页应用回退处理改变了内容。

请求排查可以按以下顺序收集证据:

  1. 请求是否符合预期:方法、URL、查询参数和请求体是否正确,是否发了多次。
  2. 谁发起请求:查看 Initiator 与调用链,区分页面代码、资源加载和重复触发路径。
  3. 服务端返回什么:状态、响应头和正文是否共同符合接口协议。
  4. 时间花在哪里:排队、连接、等待响应和下载分别占用多少时间,再结合服务端证据判断。
  5. 结果被怎样使用:浏览器策略、解析异常、状态覆盖或渲染失败是否发生在返回之后。

4.2 缓存与环境对照​

禁用缓存、保留导航日志、模拟较慢网络都可以帮助验证假设,但它们会改变环境。需要记录这些开关是否开启,并在接近真实用户条件时再次检查。

HTTP 缓存、Service Worker 缓存和页面自己的数据缓存不是同一层。一次强制刷新恢复,不能证明问题一定是浏览器 HTTP 缓存。入口 HTML、脚本分包与 Source Map 也要对应正确版本,资源发布问题见 前端资源发布。

网络面板能说明浏览器观察到的请求过程,不能单独证明数据库已经提交。显示取消或浏览器读取受限,也不能据此断定服务端没有收到或执行请求。

5. DOM、样式与白屏​

5.1 从最终样式回查​

样式不符合预期时,先选中实际元素,检查它是否就是想修改的节点,再查看 Styles 中的匹配规则与 Computed 中的最终值。规则未匹配、被覆盖、继承结果和布局约束是不同原因。

继续检查盒子尺寸、布局容器、定位参照、溢出裁剪和层叠关系。元素“看不到”可能是没有布局盒子、位于视口外、透明、被裁剪或被覆盖;仅提高 z-index 不能解决所有情况。

临时修改样式可以验证假设,但不会自动修改项目源码。确认有效后,应在源码中修复对应规则,并重新构建、加载和复现,避免把 DevTools 临时覆盖当成已经交付的修复。

5.2 按阶段排查白屏​

白屏是用户看不到预期内容的现象,可能发生在多个阶段。

阶段先检查的证据
文档与资源加载主文档状态与正文,脚本、样式和分包是否获取成功
JavaScript 执行语法兼容、运行异常、模块加载与初始化是否成功
路由与应用状态当前地址是否匹配,权限与加载状态是否进入预期分支
DOM 与呈现根节点和内容是否存在,尺寸、隐藏、遮挡和裁剪情况
特定设备或环境浏览器能力、视口、资源版本、网络、缓存与环境数据差异

200、没有 Console 错误、DOM 中有根节点,都不足以单独证明页面可用。加载中状态一直不结束,也可能表现为白屏,而不是一个未捕获异常。

无法直接打开用户设备调试窗口时,可以依靠版本标识、资源加载记录、必要的初始化阶段信息、错误栈与经过授权的复现材料缩小范围,再构造相同条件。具体采集边界见 前端可观测性。

6. 性能、内存与存储证据​

Performance 适合记录一段加载或交互,观察网络、主线程任务、布局、绘制与交互延迟之间的关系。先明确“首屏慢”还是“点击响应慢”,再选择指标与时间区间;降低网络速度和 CPU 速度模拟的是不同约束。指标与真实用户统计见 Web 性能指标。

Memory 适合比较对象分配和保留关系。页面操作后内存上升可能只是暂时分配,持续泄漏需要在相同动作和合理回收条件下比较,并查看对象被谁保留。闭包、未清理监听器和缓存只是候选原因,具体分析见 JavaScript 内存管理。

Application 面板可以检查当前源的存储、Service Worker 与相关缓存。要区分当前页面所属源、存储分区、账号状态和生命周期;看到一条旧值,不代表所有请求都在使用它。清理前先保存必要证据,避免把复现条件一并消掉。

7. 线上问题与修复验证​

线上问题首先要对齐同一版本。错误栈里的文件名和行列号对应的是实际发布产物,Source Map 才能把它映射回对应源码;随便拿当前分支的 map 可能定位到错误位置。格式化压缩代码只改善阅读,不等于还原原始 TypeScript 或组件源码。

如果暂时无法复现,可以比较受影响与正常用户的浏览器、发布版本、路由、网络结果和关键状态差异,再提出可验证的假设。需要增加观测时,记录有明确用途的字段,并限制敏感内容、采样和容量,不把“把所有数据都上报”当成调试方案。

完成修复后,重新执行原触发步骤,确认失败不再出现,并检查重复操作、错误路径和受影响的边界。若问题只在生产特定条件下出现,本地验证通过仍有范围限制,应使用对应环境的后续证据确认结果,不能只凭本地刷新一次就宣布根因已经解决。

8 · 面试题卡片​

本页题目与参考回答

8.1 遇到前端 Bug,怎样组织排查流程?​

考察重点:复现、假设、证据和回归。

相关内容:复现与最小假设、线上问题与修复验证。

参考回答

先记录触发步骤、预期、实际结果与环境,再选择能区分候选原因的观察点。 根据 Console、断点、请求或渲染记录逐步缩小范围,一次控制少量变量。修复应解释原现象,并用相同操作与相关边界验证;刷新恢复或错误暂时消失不能单独证明根因。

8.2 除了 console.log,什么时候使用哪些断点?​

考察重点:暂停入口、作用域和调用关系。

相关内容:断点、作用域与调用栈。

参考回答

已知可疑位置用行或条件断点,异常来源不明用异常断点,DOM 被修改用 DOM 断点,请求意外发出用 XHR/fetch 断点。 暂停后检查作用域和调用栈,按需要单步。注意求值可能有副作用,暂停也会改变时序,竞态问题可能需要日志或自然运行记录辅助。

8.3 开发中怎样查看和排查网络请求?​

考察重点:请求协议、响应内容、解析与后续处理。

相关内容:请求、响应与发起者。

参考回答

在 Network 中联合检查请求方法与参数、最终地址、状态、响应头与正文、发起者和时序。 再结合错误栈检查解析和状态更新。200 不能保证正文就是应用期待的数据,例如接口可能返回 HTML 或字段不符合协议;网络成功与业务处理成功应分开判断。

8.4 样式已经写了但没有生效,怎样定位?​

考察重点:匹配、层叠、最终值和布局约束。

相关内容:从最终样式回查。

参考回答

先确认实际节点,再从 Computed 的最终值回查 Styles 中哪些规则匹配、被覆盖或继承。 接着检查容器布局、尺寸、定位、裁剪和层叠。临时覆盖可以验证假设,确认后仍要改源码并重新加载验证,不能把调试面板的临时修改当作完成修复。

8.5 页面在部分设备白屏,本地正常,怎样排查?​

题型:综合题

考察重点:阶段划分、设备差异和远端证据。

相关内容:按阶段排查白屏、线上问题与修复验证。

参考回答

按资源加载、脚本执行、应用状态和最终呈现逐段查证,再对比受影响设备与正常设备的条件。 检查浏览器能力、资源版本、分包与缓存、路由和初始化记录。无法直接调试设备时,用必要的日志、错误栈与版本信息缩小范围,构造对应环境;没有未捕获异常不等于没有白屏。

8.6 页面慢或内存持续增长,怎样选择工具和证据?​

考察重点:性能记录、指标与对象保留关系。

相关内容:性能、内存与存储证据。

参考回答

先定义慢的是加载还是交互,再用 Performance 记录网络等待、主线程和渲染活动;内存问题则比较分配与保留关系。 结合对应指标和相同复现动作建立对照。一次内存上涨不证明泄漏,看到长任务也要继续定位实际执行内容,不能直接归因于某个框架或所有动画。

8.7 线上 Bug 本地无法复现,下一步做什么?​

考察重点:版本一致性、环境差异和观测成本。

相关内容:线上问题与修复验证。

参考回答

先补齐发布版本、浏览器、路由、操作与请求等最小证据,并确保错误栈对应正确的 Source Map。 对比异常与正常环境,构造有针对性的复现条件。必要时增加受控观测,避免采集无关敏感数据;本地通过后仍要说明验证范围,并用对应环境证据确认修复。

8.8 按钮第二次点击加了两次,为什么添加节流未必解决根因?​

考察重点:事件注册、函数身份与生命周期。

相关内容:重复监听示例、用同一操作验证修复。

参考回答

如果根因是 render 重复注册新的监听器,节流只改变部分触发行为,没有消除重复注册。 应验证处理函数进入次数和监听器数量,把注册与渲染分离,并在卸载时清理。用原来的连续点击和重复挂载条件检查修复,而不是只看单次点击正常。

9. 参考资料​

来源:Coding 101 原文 · 免费阅读,引用请保留出处。内容使用说明