前端调试技巧
同样一个 bug,有人改了十次代码还没找到原因,有人三分钟就定位了。差距往往不在于对工具多熟悉,而在于方法:前者看到问题就开始改代码“试试看”,后者先观察、再假设、最后验证。
这篇先讲一套通用的排查方法,再介绍 DevTools 里最有用的几种工具,最后用一个真实场景完整走一遍。
读完这篇你会
- 用“复现 → 缩小范围 → 观察 → 假设 → 验证”的流程排查问题
- 用好条件断点、DOM 断点、请求断点、异常断点
- 用网络面板判断是前端问题还是接口问题,并在本地模拟接口返回
1. 原理:调试就是不断缩小范围
调试和查电路故障是一回事。一长串灯泡不亮了,最快的办法不是挨个换灯泡,而是从中间断开,看问题在前一半还是后一半,然后对有问题的那一半继续对半分。每一步都让“可能的原因”减少一半。
text
1. 复现:能稳定复现,才能验证修复是否有效
2. 缩小范围:二分法——注释掉一半代码、回退到上一个版本、换一份数据
3. 观察:看实际的值,而不是你以为的值
4. 假设:提出一个具体的、可以被验证的猜测
5. 验证:只改一处,看现象是否随之改变最常见的反模式是跳过 1~3,直接改代码看效果。每次改动都引入新的变化,问题会越来越乱。
最容易卡住的时刻,往往是你心里想着“这里肯定没问题”。放下这个判断,去看它的实际值。
2. 先让 bug 稳定复现
“偶发”的 bug 通常只是触发条件没找到。这些手段能让很多偶发问题变成必现:
| 手段 | 能暴露什么 |
|---|---|
| Network 面板把网速调成 Slow 4G | 竞态、重复提交、加载状态没处理 |
| Performance 面板把 CPU 降速 4~6 倍 | 时序问题、低端手机上的卡顿 |
| 无痕窗口 / 清空本地存储 | 依赖缓存、旧数据导致的问题 |
| 换一个账号、换一份数据 | 权限、空数据、超长数据导致的问题 |
| 快速连续操作 | 并发、状态没重置 |
能复现后,再尽量把它缩成最小复现:删掉无关的组件和数据,直到只剩下触发问题必需的部分。很多时候,缩到最小的过程中原因就自己浮现了。
3. 断点:比 console.log 强在哪
console.log 只能看你事先想到要打印的值;断点能让程序停在某一刻,看到这一刻所有变量的值、调用栈,还能一步一步往下执行。
| 断点类型 | 怎么设置 | 适合什么场景 |
|---|---|---|
| 普通断点 | Sources 面板点击行号 | 看某一行执行时的状态 |
| 条件断点 | 右键行号 → Add conditional breakpoint | 循环第 500 次才出错:条件写 i === 500 |
| 日志断点 | 右键行号 → Add logpoint | 不改代码、不暂停,只打印(相当于临时 console.log) |
| DOM 断点 | Elements 面板右键元素 → Break on | 找出是哪段代码修改、删除了这个元素 |
| 请求断点 | Sources → XHR/fetch Breakpoints | 某个接口被调用时暂停,看是谁发的请求 |
| 事件断点 | Sources → Event Listener Breakpoints | 点击、按键时暂停,找到真正处理事件的代码 |
| 异常断点 | Sources → 勾选 Pause on caught exceptions | 错误被某个 catch 吞掉时,也能停在抛出的位置 |
停在断点后最常用的几个区域:
- Scope:当前作用域里所有变量的值,包括闭包捕获的变量;
- Call Stack:是谁一层层调用到这里的,点击任意一层可以跳过去看当时的状态;
- Watch:添加表达式,每次暂停时自动计算。
单步执行时总是钻进框架和第三方库的源码?在 Call Stack 里右键对应的文件,选择 “Add script to ignore list”,以后单步就会跳过它们。
4. 网络面板:先判断是谁的问题
很多“前端 bug”其实是接口问题,反过来也一样。打开 Network 面板,找到相关的请求,依次看:
| 看什么 | 能判断什么 |
|---|---|
| 请求是否发出、发了几次 | 没发 → 前端逻辑问题;发了两次 → 重复提交 |
| 请求的 URL、参数、请求体 | 参数不对 → 前端问题 |
| 状态码 | 4xx 多为请求本身有问题,5xx 是服务端出错 |
| 响应体 | 数据为空、字段缺失、格式不对 → 接口问题 |
| Timing | 等待服务端响应(Waiting for server response)很长 → 后端慢 |
几个实用操作:
- Preserve log:页面跳转后保留请求记录,排查登录跳转、表单提交后跳页的问题时必开;
- Disable cache:排除缓存干扰;
- 右键请求 → Copy → Copy as cURL / Copy as fetch:把完全相同的请求复制出来,发给后端同事复现,或在控制台里重放;
- 本地覆盖响应:右键请求 → Override content,可以修改接口返回的内容并保存在本地。刷新页面后,浏览器会用你修改后的响应。用它来模拟“接口返回空数组”“字段为 null”“返回 500”,不需要后端配合就能测试前端的各种分支。
5. 一个完整的排查案例
问题:用户反馈“编辑资料后点保存,偶尔会提示保存成功,但刷新后资料没变”。
- 复现:在本地操作十次都正常。把网速调成 Slow 4G 后再试,第三次就出现了。说明和网络速度有关。
- 观察:Network 面板里,一次“保存”发出了两个
PUT /profile请求,第二个请求的请求体里是修改前的旧数据。 - 缩小范围:给这个接口加请求断点,两次暂停时分别看 Call Stack。第一次来自点击“保存”按钮;第二次来自一个“离开页面前自动保存”的逻辑,它在保存成功、页面跳转时触发了。
- 假设:自动保存读取的是组件初始化时缓存的表单数据(旧值),在慢网络下它比正常保存更晚到达服务器,于是用旧数据覆盖了新数据。
- 验证:在自动保存的函数里打断点,Scope 中的表单数据确实是旧值。修复方式是:自动保存读取最新的表单状态,并且在一次保存进行中时不再发起新的保存。修复后用 Slow 4G 重复操作二十次,问题不再出现。
整个过程没有“猜着改”,每一步都基于观察到的事实。
6. 线上问题怎么查
线上问题无法打断点,要靠事先准备好的信息:
- 错误监控 + Source Map:线上代码是压缩过的,上传 Source Map 到监控平台,堆栈才能还原到源码行(不要把 Source Map 公开部署);
- 有上下文的日志:
js
// ❌ 看不出发生了什么
console.log('error', err)
// ✅ 带上操作、关键参数和错误信息(不要记录密码、token 等敏感信息)
console.error('[profile] save failed', { userId, fields: Object.keys(changes), message: err.message })- 找出是哪次提交引入的:问题在某个版本之后才出现,但不知道是哪次改动?用
git bisect做二分查找:
bash
git bisect start
git bisect bad # 当前版本有问题
git bisect good v1.4.0 # 这个版本是好的
# Git 会切到中间的某个提交,你测试后告诉它结果:
git bisect good # 或 git bisect bad
# 重复几次后,Git 会指出第一个出问题的提交
git bisect reset # 结束,回到原来的分支一百个提交,最多测七次就能找到元凶。
移动端和 App 内置浏览器的调试方法,见 移动端适配 的排查部分。
7. 常见现象速查
| 现象 | 先查什么 |
|---|---|
| 数据没显示 | Network 里接口的响应是不是空的、字段名对不对 |
| 样式没生效 | Elements 面板里规则是否被划掉(被覆盖),选择器是否匹配 |
| 点击没反应 | 控制台有没有报错;元素是否被透明层遮挡(Elements 面板悬停查看) |
| 只有部分用户出问题 | 按浏览器版本、账号权限、数据特征分组对比 |
| 偶发问题 | 慢网络、CPU 降速、快速重复操作 |
| 页面越用越卡 | Memory 面板拍堆快照对比,查找泄漏 |
| 错误被吞掉了 | 勾选 Pause on caught exceptions |
检查清单
- [ ] 先稳定复现,必要时缩成最小复现
- [ ] 观察实际值,不靠“应该是这样”的判断
- [ ] 每次只改一处,验证现象是否改变
- [ ] 会用条件断点、DOM 断点、请求断点和异常断点
- [ ] 先用 Network 面板区分前端问题和接口问题
- [ ] 线上有错误监控、Source Map 和有上下文的日志
- [ ] 修复后用当初能复现问题的条件再验证一遍
延伸阅读
- 错误处理与异常:让错误可见、可上报
- 异步流程与 Promise:竞态和重复提交的原理
- 日志、指标与可观测性:线上问题的数据基础