把网络限速到 3G 之后我才发现,那些通过了所有测试的代码其实到处是坑
直接给结论:别总在光纤环境下测性能,尝试在 Chrome DevTools 里把网络切到 3G(现在 Chrome 把 Slow 3G 改名叫 3G 了,就在 Slow 4G 和 Offline 之间)。在这种环境下,很多平时被低延迟掩盖的「正确性 Bug」会直接现形,尤其是那些涉及异步请求的竞态条件。
我之前在本地 WiFi 测的时候,App 运行得顺风顺水,结果一开 3G 限速,立马崩了两个地方。最恶心的是,这两个 Bug 所在的函数在我的单元测试里全部是 Pass 状态。
搜索结果顺序反了,这就是典型的竞态条件
最典型的一个坑出在搜索框上。代码写得极其简单,就是典型的 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 前,强制自己用 3G 模式跑一遍核心链路,能抓到很多原本在测试环境下根本看不到的低级错误。

这波操作太绝了,赶紧把 3G 模拟跑一遍,看看我那几个接口会不会直接崩掉...