跳到主要内容

你在项目中遇到的最难解决的 Bug

问题

你在项目中遇到过最难解决的 Bug 是什么?你是怎么排查和解决的?

回答思路

1. 面试官的考察点

这道题和"最有挑战的技术难题"有一定交叉,但更聚焦于 Bug 排查能力。面试官重点关注:

考察维度说明
排查方法论是否有系统性的调试思路,而不是"瞎猜"
工具使用能力是否熟练运用 DevTools、日志、监控平台
底层原理理解能否从现象追溯到根因(浏览器行为、框架机制、系统层面)
复盘总结能力修完 Bug 之后做了什么预防措施
沟通与协作跨端/跨团队 Bug 时如何协调排查
和"最有挑战的技术难题"的区别

"技术难题"更偏向架构设计、性能优化、方案选型等主动挑战;"最难的 Bug"更偏向被动遇到的、难以复现的、排查过程曲折的问题。面试时两道题准备不同的案例。

2. 什么样的 Bug 称得上"最难"

面试官心中"难解的 Bug"通常具备以下一个或多个特征:

3. 系统性排查方法论

面试时务必展示一套 结构化的排查流程,而非"试了很多方法最后碰巧解决了":

阶段具体动作常用工具
收集信息错误日志、用户反馈、监控告警、截图/录屏Sentry、日志平台、用户工单
稳定复现找到最小复现路径,确定复现率和触发条件无痕模式、多设备测试、Charles 抓包
缩小范围二分法定位:前端 vs 后端、代码 vs 环境、新代码 vs 旧代码git bisect、条件断点、Network 面板
形成假设根据现象和经验提出 2-3 个可能的根因经验 + 文档 + 搜索
验证假设针对每个假设设计实验验证console.log、断点、Proxy 拦截、抓包
修复验证修复后在原复现路径上验证,确认不引入新问题回归测试、灰度发布
复盘预防增加监控/测试/文档,防止同类问题再出现单元测试、E2E、告警规则

4. 经典 Bug 案例

案例一:iOS Safari 偶现白屏(移动端兼容类)

**现象**:React SPA 在 iOS Safari 中约 5% 概率白屏,
无 JS 错误日志,杀进程后重进恢复正常。

**排查过程**
1. Sentry 无 JS 错误 → 排除代码异常
2. 远程调试 Safari → 发现白屏时 DOM 为空
3. 怀疑 Service Worker 缓存问题 → 禁用 SW 后白屏率下降但未完全消失
4. 进一步发现 Safari 的 bfcache 机制 → 前进/后退时恢复了过期的页面状态
5. 根因:bfcache 恢复了旧页面,而 SPA 路由状态已失效,导致空渲染

**解决方案**
- 监听 pageshow 事件,检测 event.persisted(bfcache 恢复)后强制 reload
- Service Worker 缓存策略从 cache-first 改为 stale-while-revalidate
- 增加白屏检测:页面加载 3s 后检查 #root 子元素数量,为 0 则自动重载

**结果**:白屏率从 5% 降至 0.01%。

**预防措施**
- 在 CI 中加入 iOS Safari 自动化测试(Playwright WebKit)
- 搭建白屏监控告警,及时发现同类问题

案例二:接口数据偶现错乱(竞态条件类)

**现象**:搜索结果偶尔显示的不是当前关键词的内容,
快速输入时更容易出现,约 10% 概率。

**排查过程**
1. 抓包对比 → 请求和响应的关键词不匹配
2. 怀疑后端问题 → 后端日志确认返回数据正确
3. 前端代码审查 → 发现快速输入时多个请求并发,
旧请求的响应在新请求之后返回,覆盖了正确的结果
4. 根因:经典的请求竞态问题,没有取消过期请求

**解决方案**
useSearch.ts
function useSearch(keyword: string) {
const [results, setResults] = useState([]);
const abortControllerRef = useRef<AbortController>();

useEffect(() => {
// 取消上一次请求
abortControllerRef.current?.abort();
const controller = new AbortController();
abortControllerRef.current = controller;

const fetchResults = async () => {
try {
const res = await fetch(`/api/search?q=${keyword}`, {
signal: controller.signal,
});
const data = await res.json();
if (!controller.signal.aborted) {
setResults(data.results);
}
} catch (err) {
if (err instanceof DOMException && err.name === 'AbortError') {
// 请求被取消,忽略
return;
}
throw err;
}
};

if (keyword) fetchResults();
return () => controller.abort();
}, [keyword]);

return results;
}
**结果**:搜索结果错乱问题完全消失。

**预防措施**
- 封装通用的 useFetch Hook,内置 AbortController 竞态处理
- Code Review 检查清单新增"并发请求竞态"检查项

案例三:生产环境样式错乱(构建/部署类)

**现象**:每次部署后约 30 分钟内,部分用户看到样式错乱,
之后自动恢复。仅影响部分用户。

**排查过程**
1. 检查 CSS 文件 → hash 正确,内容无问题
2. 对比用户请求 → 部分用户加载了旧版 CSS 配新版 HTML
3. 检查 CDN 缓存 → CDN 节点缓存刷新有延迟
4. 根因:部署时先上了新 HTML(引用新 CSS hash),
但 CDN 边缘节点还在缓存旧 CSS 文件,
导致新 HTML 引用的 CSS 文件 404 后 fallback 到旧版

**解决方案**
- 部署顺序改为"先上静态资源,再上 HTML"
- 静态资源保留多版本(不删除旧版本文件)
- HTML 设置 Cache-Control: no-cache,CSS/JS 设置长期缓存 + 内容 hash
- CDN 预热:部署后主动触发热门节点的缓存刷新

**结果**:部署后样式错乱问题完全消失。

**预防措施**
- 部署流水线加入静态资源可用性检查
- 灰度发布:先 1% 流量验证,再全量

案例四:内存泄漏导致页面崩溃(内存类)

**现象**:管理后台在使用 2-3 小时后越来越卡,
最终浏览器标签页崩溃。重新打开后又正常。

**排查过程**
1. Performance Monitor → 内存持续上涨,不回落
2. Heap Snapshot 对比 → 发现大量 Detached DOM 节点
3. 定位到一个全局 EventBus → 组件 mount 时注册事件,
unmount 时没有取消注册
4. 每次路由切换都累积一批"幽灵"事件监听,
这些监听闭包引用了已卸载组件的 DOM 和 state

**解决方案**
useEventBus.ts
function useEventBus(event: string, handler: (...args: unknown[]) => void) {
useEffect(() => {
eventBus.on(event, handler);
// 组件卸载时必须取消订阅
return () => {
eventBus.off(event, handler);
};
}, [event, handler]);
}
**结果**:内存占用从持续上涨变为稳定在 200MB 以内。

**预防措施**
- 禁止组件中直接使用 eventBus.on(),统一使用 useEventBus Hook
- 搭建内存泄漏自动化检测:CI 中运行页面 50 次路由切换后检查内存增量
- ESLint 自定义规则:检测 useEffect 中的订阅是否有对应的清理函数

案例五:WebSocket 消息丢失(网络/并发类)

**现象**:直播间弹幕消息偶尔丢失,用户 A 发送的弹幕,
用户 B 有时看不到。后端日志确认消息已推送。

**排查过程**
1. 抓包对比 → WebSocket 帧确实收到了消息
2. 前端日志 → onmessage 回调确实被触发
3. 定位到消息处理逻辑 → 短时间内大量消息导致 React 批量更新,
state 更新时使用了 setState(newList) 而非函数式更新
4. 根因:快速连续的 setState 在批处理中只保留了最后一次的值

**解决方案**
useDanmu.ts
// ❌ 错误:多次快速 setState,只有最后一次生效
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
setMessages([...messages, msg]); // messages 是闭包中的旧值
};

// ✅ 正确:使用函数式更新,基于最新 state
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
setMessages((prev) => [...prev, msg]);
};
**结果**:弹幕消息丢失率从约 8% 降到 0%。

**预防措施**
- Code Review 规则:WebSocket/定时器回调中的 setState 必须使用函数式更新
- 封装 useWebSocket Hook,统一消息缓冲和状态更新逻辑

5. 回答的注意事项

常见扣分点
  1. 没有排查过程:直接说"我搜了一下发现是 XX 问题",看不到分析能力
  2. Bug 太简单:比如"拼写错误导致的报错",不能体现深度
  3. 只说了修复没说预防:资深工程师应该有"修一个防一类"的意识
  4. 无法解释根因:修好了但说不清为什么,说明缺乏底层理解
加分回答模式
  1. 展示排查工具链:Performance / Memory / Network 面板、Sentry、Charles、远程调试
  2. 体现系统性思维:从监控到告警到修复到预防的完整闭环
  3. 多走了一步:修完 Bug 后还做了通用化的工具/规范,惠及整个团队
  4. 诚实谈弯路:排查过程中的错误方向反而体现真实性和反思能力

6. 如何准备

准备 2-3 个不同类型的 Bug 案例,根据面试场景灵活选择:

Bug 类型适合展示的能力推荐场景
偶现白屏 / 崩溃移动端调试、监控体系移动端 / 跨端岗位
竞态条件 / 数据错乱异步编程理解、并发处理高级前端 / 架构岗位
构建部署导致的问题工程化理解、CI/CD 经验工程化 / 基建方向
内存泄漏 / 性能退化性能分析、底层原理性能优化方向
浏览器兼容问题跨浏览器经验、标准理解2C 业务 / 移动端
跨端通信问题WebSocket / JSBridge 经验全栈 / 实时应用方向

常见面试问题

Q1: 你排查 Bug 的一般流程是什么?

答案

我的排查流程分 7 步:

  1. 收集信息:先看错误日志和监控,了解影响范围和复现条件
  2. 稳定复现:找到最小复现路径,没有稳定复现一切都是猜测
  3. 缩小范围:用二分法缩小范围 —— 是前端还是后端?是哪个版本引入的?用 git bisect 精确定位到问题 commit
  4. 形成假设:根据现象提 2-3 个可能的原因,按可能性排序
  5. 逐一验证:针对每个假设设计实验,比如加日志、mock 数据、条件断点
  6. 修复验证:修复后在原场景验证,并检查是否有副作用
  7. 复盘预防:写 postmortem,增加测试用例或监控告警
关键原则

排查 Bug 最重要的是 "不要猜,要证明"。每一步都应该有证据支撑,而不是"感觉可能是这个问题"。

Q2: 遇到无法复现的 Bug 怎么办?

答案

无法复现是最头疼的情况,我的策略是:

  1. 增加信息采集:在可疑代码路径上增加详细日志,包括入参、中间变量、环境信息
  2. 缩小环境差异:对比能复现和不能复现的环境(浏览器版本、设备、网络、登录状态)
  3. 构造极端条件:用工具模拟弱网(Charles 限速)、高并发(循环请求)、边界数据
  4. 代码审查:静态审查可疑代码,特别关注竞态条件、闭包变量、异步时序
  5. 防御性修复:如果确信是某类问题但无法 100% 复现,做防御性处理并增加监控
防御性修复示例
// 即使不确定根因,也先做防御性处理 + 日志
function processData(data: unknown) {
if (!data || typeof data !== 'object') {
// 记录异常日志,帮助后续定位
logger.warn('processData received unexpected data', {
type: typeof data,
value: JSON.stringify(data)?.slice(0, 200),
stack: new Error().stack,
});
return fallbackData;
}
// 正常处理逻辑...
}

Q3: 你修完 Bug 后一般会做什么?

答案

修完 Bug 只是开始,资深工程师的价值在于 "修一个防一类"

  1. 写回归测试:针对这个 Bug 写单元测试或 E2E 测试,防止复发
  2. 加监控告警:如果是生产环境 Bug,添加对应的监控指标和告警规则
  3. 更新文档:在团队知识库记录这个 Bug 的排查过程和解决方案
  4. 审查同类代码:搜索代码库中是否有相同模式的隐患代码,统一修复
  5. 工具化/规范化:如果是通用性问题,封装工具函数或增加 ESLint 规则
示例:封装通用 Hook 防止竞态问题再发生
// 修完竞态 Bug 后,封装通用的 useLatestFetch
function useLatestFetch<T>(fetcher: () => Promise<T>, deps: unknown[]) {
const [data, setData] = useState<T>();
const [error, setError] = useState<Error>();

useEffect(() => {
const controller = new AbortController();
let cancelled = false;

fetcher()
.then((result) => {
if (!cancelled) setData(result);
})
.catch((err) => {
if (!cancelled) setError(err);
});

return () => {
cancelled = true;
controller.abort();
};
}, deps);

return { data, error };
}

Q4: 你遇到过跨团队的 Bug 吗?怎么协调排查的?

答案

跨团队 Bug 排查的关键是 用数据说话,而不是互相甩锅

  1. 明确界面:通过抓包对比请求和响应,确定问题在前端还是后端
  2. 提供完整信息:给对方提供复现步骤、请求 ID、时间戳、截图/录屏
  3. 协同调试:组织联调会议,各端同时打日志,对比时序
  4. 建立共识:用 timeline 图展示各端的行为时序,让所有人看到全貌

Q5: 你怎么看待"先上线再修 Bug"和"修完再上"的取舍?

答案

这是工程实践中的经典权衡,我的判断标准:

维度先上线再修修完再上
Bug 严重程度不影响核心功能、仅影响少量用户影响核心流程、数据安全、资金相关
修复时间修复需要较长时间(> 1 天)能快速修复(< 2 小时)
业务压力业务有 deadline、竞品压力没有紧迫的上线需求
降级方案有兜底方案(Feature Flag 关闭)没有有效的降级手段
底线原则

无论如何,以下情况 必须修完再上

  • 涉及数据安全和用户隐私
  • 涉及资金交易
  • 会导致数据不可逆损坏

Q6: 有没有一个 Bug 让你从根本上改变了编码习惯?

答案

这是一个很好的反思题,可以从以下角度回答:

"之前我写 useEffect 时经常忘记清理函数(取消订阅、abort 请求),直到有一次线上出现内存泄漏,排查了整整两天才定位到一个没有 unsubscribe 的 EventBus 监听。从那以后,我养成了 写 useEffect 先写 return 清理函数,再写正逻辑 的习惯。"

// 我的编码习惯:先写清理,再写逻辑
useEffect(() => {
const controller = new AbortController();
// 先写 cleanup
const cleanup = () => {
controller.abort();
};

// 再写正逻辑
fetchData(controller.signal);

return cleanup;
}, []);

这个回答好在:

  1. 具体 —— 有真实场景
  2. 有因果 —— 因为踩坑所以改变
  3. 可验证 —— 面试官能从代码风格中看到这个习惯

Q7: 用过哪些工具排查 Bug?分别适合什么场景?

答案

工具适用场景使用要点
Chrome DevTools - Performance页面卡顿、Long Task录制 → 看火焰图 → 定位耗时函数
Chrome DevTools - Memory内存泄漏、页面崩溃Heap Snapshot 对比 → 找 Detached DOM
Chrome DevTools - Network请求异常、缓存问题看请求时序、Response Headers
React DevTools ProfilerReact 重渲染问题看组件渲染次数和耗时
Sentry生产环境错误监控sourcemap + 用户上下文
Charles / Whistle抓包、mock 数据HTTPS 代理、弱网模拟
Safari Web InspectoriOS 真机调试USB 连接 + Safari 远程调试
git bisect定位引入 Bug 的 commit二分法缩小范围,效率极高
Source Map Explorer产物分析定位体积异常的依赖
# git bisect 实战:快速定位引入 Bug 的 commit
git bisect start
git bisect bad # 当前版本有 Bug
git bisect good v2.1.0 # 这个版本没有 Bug
# Git 自动 checkout 中间版本,你测试后标记 good/bad
git bisect good # 这个中间版本没问题
git bisect bad # 这个有问题
# 最终 Git 告诉你是哪个 commit 引入的

Q8: 你怎么预防 Bug 的出现?

答案

从四个层面建立防御体系:


相关链接