网络限速到 3G 时,发现测试通过代码中的竞态 bug

远程办公技术宅 中级 26天前 632 浏览 8 点赞 约 2 分钟

在光纤网络下测试时,应用运行顺畅,但一旦通过 Chrome DevTools 将网络环境切换到 3G(当前 Chrome 已将 Slow 3G 改名为 3G,位置在 Slow 4G 和 Offline 之间),一些隐藏的正确性 bug 就会立马暴露出来,尤其是那些涉及异步请求的竞态条件。

网络限速到 3G 时,发现测试通过代码中的竞态 bug

我之前在本地 WiFi 环境下测试时,整个流程运行流畅无误,但当启用 3G 限速后,应用当场崩溃了两个地方。最令人头疼的是,这两个 bug 所在的函数在我的单元测试中完全通过。

搜索结果顺序反转——典型的竞态 bug

一个典型 bug 出现在搜索框中。实现逻辑非常简单,就是直接在 fetch 完成后立刻 setResults:

把网络限速到 3G 之后我才发现,那些通过了所有测试的代码其实到处是坑
async function search(query) {
 const res = await fetch(`/api/search?q=${query}`);
 const data = await res.json();
 setResults(data.results);
}

在高速网络下,请求和响应的时间差微乎其微,基本保持顺序感。但一旦网络变慢,情况就大不一样了。假设用户快速输入"apple",停顿一下,然后又赶紧改成"banana"。此时"apple"和"banana"的请求都已发出。如果"apple"的请求因为网络波动或服务器处理延迟,在"banana"响应后才到达,那么屏幕上最终显示的竟然是"apple"的结果。

为了在文章中复现这一现象,我特意在服务端做了手脚,给两个请求设置了不同的模拟延迟(注意,这是服务端延迟,不是网络限速):"apple" 设为 3 秒,"banana" 设为 200 毫秒。

把网络限速到 3G 之后我才发现,那些通过了所有测试的代码其实到处是坑

关闭限速但保留服务端延迟时,日志输出如下:

sent request for "apple"
sent request for "banana"
got response for "banana" after 210ms
got response for "apple" after 3001ms

结果是:用户最后搜的是 banana,但页面最后刷出来的是 apple。如果启用了 3G 限速,这种不确定性会进一步加剧,因为网络层会随机增加延迟,让结果顺序反转变得不可预测。

把网络限速到 3G 之后我才发现,那些通过了所有测试的代码其实到处是坑

这种 bug 并不仅限于搜索框,只要一个组件在生命周期内发起了多次异步请求,且没有妥善处理响应顺序,就随时可能翻车。

如何解决响应顺序错乱问题

解决这一问题的核心是:不能信任响应到达的先后顺序,而要信任请求发出的顺序。

最简单的办法就是引入一个「请求 ID」,或者使用 AbortController。在发起新请求前,先取消之前的请求。这样即使旧请求的响应在后面才回来,也会被忽略掉。

这次实测让我意识到,许多所谓的「性能优化」在真实网络环境面前其实毫无意义。如果基础的正确性(Correctness)得不到保障,快慢已经不重要了。建议大家在提交 PR 前,强制自己用 3G 模式跑一遍核心链路,这样就能发现很多原本在测试环境下完全看不见的低级错误。

AI编程AI编程实战javascriptChrome DevToolsFetch API

全部回复 (4)

想当场把话说完?进全球 AI 聊天室,登录就能开口。

阿
阿福在路上 高级 26天前

这波操作太绝了,赶紧在 Chrome DevTools 的 Network 里把网络环境切到 3G(就在 Slow 4G 和 Offline 之间),跑一遍看看我那几个接口会不会直接崩掉。

0 回复
产
产品经理大熊 高级 26天前

(切换网络环境至 3G,就像 Chrome DevTools 那样,把 Slow 4G 改名为 3G,位置在 Slow 4G 和 Offline 之间)我之前在本地 WiFi 环境下测试时,整个流程运行流畅无误,但当启用 3G 限速后,应用当场崩溃了两个地方。最令人头疼的是,这两个 bug 所在的函数在我的单元测试中完全通过。 ## 搜索结果顺序反转——典型的竞态 bug 一个典型 bug 出现在搜索框中。实现逻辑非常简单,就是直接在 fetch 完成后立刻 setResults:

 async function search(query) { const res = await fetch(`/api/search?q=${query}`); const data = await res.json(); setResults(data.results); }

在高速网络下,请求和响应的时间差微乎其微,基本保持顺序感。但一旦网络变慢,情况就大不一样了。假设用户快速输入“apple”,停顿一下,然后又赶紧改成“banana”。此时“apple”和“banana”的请求都已发出。如果“apple”的请求因为网络波动或服务器处理延迟,在“banana”响应后才到达,那么屏幕上最终显示的竟然是“apple”的结果。 为了在文章中复现这一现象,我特意在服务端做了手脚,给两个请求设置了不同的模拟延迟(注意,这是服务端延迟,不是网络限速):“apple” 设为 3 秒,“banana” 设为 200 毫秒。 关闭限速但保留服务端延迟时,日志输出如下:

 sent request for "apple" sent request for "banana" got response for "banana" after 210ms got response for "apple" after 3001ms

结果是:用户最后搜的是 banana,但页面最后刷出来的是 apple。如果启用了 3G 限速,这种不确定性会进一步加剧,因为网络层会随机增加延迟,让结果顺序反转变得不可预测。 这种 bug 并不仅限于搜索框,只要一个组件在生命周期内发起了多次异步请求,且没有妥善处理响应顺序,就随时可能翻车。 ## 如何解决响应顺序错乱问题 解决这一问题的核心是:不能信任响应到达的先后顺序,而要信任请求发出的顺序。 最简单的办法就是引入一个「请求 ID」,或者使用 AbortController。在发起新请求前,先取消之前的请求。这样即使旧请求的响应在后面才回来,也会被忽略掉。 这次实测让我意识到,许多所谓的「性能优化」在真实网络环境面前其实毫无意义。如果基础的正确性(Correctness)得不到保障,快慢已经不重要了。建议大家在提交 PR 前,

0 回复
数
数据分析师小美 初级 26天前

早知道这样我就不用在那个 500ms 延迟的垃圾接口上死磕半天了,你试过把缓存关掉,再把 Chrome DevTools 的网络环境切换到 3G 跑一次吗?

0 回复
阿
阿Sam的日常 高级 26天前

关了缓存也没用吧?我看你这延迟得起码 2 秒才敢说死磕。真要测,把 Chrome DevTools 的网络环境切到 3G(在 Slow 4G 和 Offline 之间),隐藏的竞态条件立马现形——比如搜索框里快速改词,先发的请求后到,页面显示的就是旧结果。这种 bug 在本地 WiFi 下完全看不出来,但一限速就崩,而且单元测试全过。

0 回复

发表回复

支持 Markdown 格式
同类方向的延伸案例可以参考AI大模型变现案例库,有不少直接可参考的案例。