接 React 18 项目的前端,几乎都被这个问题咬过:开发环境点一次按钮,实时流量监控里却看到两条事件上报,比预期多了一倍。代码翻来覆去检查,useEffect 清得干干净净,removeEventListener 也成对写了,还是找不到原因。
先把三个最常见的误区摆出来,再逐一澄清:
- 误区一:「开发环境都翻倍了,生产环境肯定也会」------错,StrictMode 双调用是开发环境专属行为。
- 误区二:「effect 跑两次一定是 bug,得赶紧改业务代码」------错,React 是故意这么设计的,用来逼你写对清理函数。
- 误区三:「开发环境数据偏高,拿它估生产流量就行」------错,开发环境数字天生比生产多一份,直接拿来估会高估一倍。
下面把这三个误区逐一拆开,给出 console 计数、Network 面板、实时流量监控三步验收法。
**直答:**开发环境点一次却上报两条,多半是React 18 StrictMode故意双调用组件。这不是bug,关键是分清开发环境与生产环境的行为差异。
先把现象钉死。复现路径很简单:打开本地起的 React 项目,登录,进入活动页,点一次报名按钮。按业务预期,实时流量监控里应该出现一条「点击报名」事件,但实际出现了两条,时间戳挨得很近,间隔不过几十毫秒。
对着点二十次,结果很稳定:本地开发环境点一次出两条,打包到测试环境点一次出一条。这个对比很关键------如果两边都多,那是代码问题;只有开发环境多,问题多半出在开发环境特有的行为上,而不是业务逻辑写错了。
第一个假设:又是重复绑定?
第一反应往往是老问题:useEffect 里 addEventListener 没清理。但翻开报名组件看,useEffect 里注册了点击监听,return 里确实写了 removeEventListener,函数引用也同一个,按说不会累积。
为了排除,在 useEffect 里临时加一行日志,刷新页面看输出。结果控制台先打一行挂载,紧接着又打一行,中间还夹着一行清理。也就是说,组件只挂载了一次,React 却把 effect 跑了两轮。
验证一:console.log 计数
加个计数器,看 effect 到底跑了几遍:
let count = 0;
useEffect(() => {
count++;
console.log('effect 执行第', count, '次');
return () => console.log('cleanup');
}, []);
刷新页面,输出是:effect 执行第 1 次,cleanup,effect 执行第 2 次。一个空依赖的 useEffect,按常规理解应该只跑一次,现在却跑了两次。这不是代码写错了,是 React 在开发环境主动这么干的。方向找错了------问题不在「绑了几次」,而在「React 让这个组件活了两次」。
验证二:DevTools Network 面板
打开 Network 面板,筛选打点请求,再点一次报名按钮。浏览器发出去两条上报请求,载荷几乎一模一样,时间戳错开几十毫秒。这就解释了为什么实时流量监控里看到两条------前端真的发了两次,不是后端重复计算,也不是看板缓存。
把生产构建跑起来对比:同一个组件,打包之后本地预览,点一次,Network 里只有一条请求。现象彻底坐实:双执行只在开发环境出现。假设收敛到「React 开发环境行为」上,剩下的就是去文档里找证据。
(如下图)
从现象到结论的排查决策树
根因找到了:StrictMode在开发环境故意双调用
去翻React官方文档,StrictMode那一节写得很明白:从React 18开始,开发环境下组件会被故意挂载、卸载、再挂载一次,用来帮你发现副作用没有清理的问题。这个「双调用」只发生在开发模式,生产构建里StrictMode不会执行这个双重渲染。
换句话说,我们看到的两次effect执行、两次事件监听器注册、两次埋点上报,是React刻意制造的压力测试。它在告诉你:你的副作用有没有写清理函数,一旦真实挂载-卸载发生,会不会泄漏。可问题是,很多同学不知道这是开发环境专属行为,看到开发环境事件翻倍,慌慌张张上线去改业务代码,甚至把已经写对的清理函数删掉,反而把生产环境搞坏了。
这里要特别强调一句:生产环境StrictMode不会双执行,这是React官方文档明确说明的。所以开发环境看到的双份埋点,默认是「正常现象」,不是bug。但前提是你的副作用写了正确的清理函数------如果没写,双执行就会把真实的监听器泄漏暴露出来,那种情况才需要修,而修法是补cleanup,不是删业务逻辑。
三个修复与防护手段
知道根因之后,处理思路就清楚了。分三层,从最该做的到可选的:
第一层,把清理函数写对。这是React希望你做的事------effect里注册了什么,return里就清掉什么。监听器、定时器、订阅,都成对出现。
useEffect(() => {
const btn = document.querySelector('#signup');
const handler = () => {
_yhxw456_trackdata.push(['event', '按钮', '报名']);
};
btn.addEventListener('click', handler);
return () => btn.removeEventListener('click', handler);
}, []);
第二层,对「只该执行一次」的初始化逻辑做去重标记。比如SDK初始化、全局埋点队列启动,这种事哪怕effect跑两遍也只能真的做一次。需要注意的是,在StrictMode下组件内的ref会被重置,单纯用useRef在开发环境仍可能跑两次真正的初始化,更稳的做法是把这类初始化提到模块顶层,或者用一个模块级变量判断,而不是依赖组件内的ref。
第三层,开发环境判断。如果某些上报确实只想在真挂载时发一次,可以在代码里判断当前是否为开发环境,开发环境跳过或仅打日志。但这只是验收辅助,不要靠它掩盖没写清理函数的问题------清理函数才是治本的那一条。
怎么区分「正常双调用」和「真泄漏」?有一个简单的判据:在effect里打日志,如果第二次effect执行之前,cleanup也同步执行了一次,说明React正确地把上一轮清理干净了,监听器没有累积,这是正常双调用;如果cleanup没跑、或者刷新几次后日志条数逐次翻倍,那才是真泄漏,要去补清理函数。把这个判据记牢,就不会再把React的提示误当成事故。
(如下图)
开发环境与生产环境的上报次数对比
验收:怎么确认生产环境是对的
排查到这一步,不等于收工。我们团队后来定了一个三步验收法,每次埋点上线前都过一遍:
第一步,console.log计数。在关键effect里打一行日志,开发环境看它是不是跑了两遍------如果跑了两遍且cleanup也跑了,说明是StrictMode的正常双调用,不用慌。
第二步,Chrome DevTools Network面板筛选打点请求。开发环境点一次,看到两条是预期;打包后在测试环境点一次,必须只有一条。这一步是验收的硬标准,两条和一条的差别一眼就能看出来。
第三步,用实时流量监控验收。456数据的实时流量监控功能免费版即可使用,开一个无痕窗口访问测试地址,点一次按钮,回到实时流量监控里看自己这条会话产生了几条事件。免费版的额度对这种自测完全够用,不用等第二天报表。
验收时记住一个口径:开发环境的数字永远比生产多一份,这是React给你的提示,不是数据错了。拿开发环境的事件数去估生产流量,会凭空高估一倍,这种误判我见过不止一次。
开发环境与生产环境差异对照
| 维度 | 开发环境(StrictMode) | 生产环境 | 应对 |
|---|---|---|---|
| 组件挂载 | 挂载→卸载→再挂载 | 只挂载一次 | 以生产环境表现为准 |
| useEffect | 执行两次 | 执行一次 | 必须写清理函数 |
| 事件监听器 | 注册两次,清理不对则泄漏 | 注册一次 | 成对removeEventListener |
| 埋点上报 | 预期两条 | 预期一条 | Network面板核对请求数 |
| 看板读数 | 偏高约一倍 | 真实值 | 用实时流量监控自测验收 |
说明:StrictMode双调用行为以React官方文档最新说明为准。
FAQ
Q1:React StrictMode为什么在开发环境双执行?
A:React 18起,StrictMode在开发环境故意把组件挂载、卸载、再挂载一次,目的是逼开发者写出能正确清理副作用的代码。这个双调用只在开发模式发生,生产构建不会执行。
Q2:生产环境StrictMode会不会也让埋点上报两次?
A:不会。React官方文档明确说明,StrictMode的双重调用是开发环境专属行为,生产构建中组件只挂载一次、effect只执行一次。开发环境看到的双份不会带到生产。
Q3:开发环境埋点翻倍是不是bug,要修吗?
A:如果effect写了正确的清理函数,双执行是React的正常提示,不是bug,不用改业务逻辑。但如果cleanup没写、监听器真的累积了,那是真问题,要补清理函数。
Q4:怎么快速验证一次点击到底发了几条上报?
A:打开Chrome DevTools的Network面板,筛选打点请求,手动点一次按钮,数发出的请求数。开发环境预期两条,生产环境预期一条。这是最直接的验收手段。
Q5:实时流量监控能用来验收埋点吗?
A:可以。用无痕窗口访问测试地址并触发事件,在实时流量监控里观察自己这次会话产生的事件流,逐条核对事件名称和次数,不用等第二天报表。
把 cleanup 再检查一遍,确认写对了,然后把开发环境那两条上报当成 React 送的「你副作用写得还行」的提示。打包上线,生产环境的事件数和预期一致。开发环境的异常,有时候不是代码错了,而是框架在用它的方式提醒你------副作用要成对,清理要及时。看懂这一层,很多「莫名其妙翻倍」的谜团,半天就能解开。
数据来源
- React 官方文档《StrictMode》
- React 官方文档《useEffect》
- MDN Web Docs《EventTarget.addEventListener()》
- Chrome DevTools 文档《Network 面板》