一、背景:某核心模块突然白屏
前两周某私有云客户反馈一个关键业务页面 突然白屏,刷新浏览器会好,但是经常复现,极其影响用户操作和体验。
由于是私有云,并且我公有云复现不出来,于是找到用户远程看问题。
打开控制台一看,迎面就是一串熟悉又危险的错误:
scala
TypeError: Converting circular structure to JSON
--> starting at object with constructor 'Object'
--- property 'xxxx' closes the circle

这意味着 ------
某处代码正在用 JSON.stringify 序列化一个包含循环引用的数据结构。
问题立刻严重起来:
- 循环引用导致 JSON.stringify 崩溃
- 这个崩溃发生在 React 生命周期
- 生命周期抛错时整个 UI 树会 unmount → 直接白屏
这是典型的 现网级事故。
二、初次定位与修复:错误指向 componentDidUpdate
进一步追踪堆栈,定位到某个类组件的 componentDidUpdate ,然后我查看源码:
js
componentDidUpdate(prevProps, prevState) {
if (JSON.stringify(xxx) !== JSON.stringify(xxx) || ...) {
}
心想一定是这里出了问题,因为堆栈原因说的很清楚了,于是开始着手修改代码。
改完发现不是私有云分支,于是切换到私有云分支之后发现,JSON.stringfy()已经被替换成 lodash.isEqual() ,但是公有云没有下沉。
js
componentDidUpdate(prevProps, prevState) {
if (!_.isEqual(
xxx.tool.copyObj(...),
xxx.tool.copyObj(...)
)) {
xxx
this.setState(xxx);
}
}
逻辑没有变化:
- 比较 props 是否变化
- 变化后 setState 触发渲染
我看问题已经被修复,于是询问了客户版本,原来是还没有升级最新补丁,于是帮助她升级补丁,验证后无问题。
三、第二次复现:isEqual 被编译成了 JSON.stringify
周一一大早用户发消息说又复现了白屏问题,然后我再次远程,查看 webpack 编译后的 bundle 时,发现真正运行的代码竟然是:
js
if (JSON.stringify(O.tool.copyObj(...)) !== JSON.stringify(O.tool.copyObj(...))) {
...
}
打包后,lodash 的 isEqual 被直接替换成了 JSON.stringify 的结果对比。
而此时报错依然是 TypeError: Converting circular structure to JSON。
此时怀疑是不是代码不对,对比 commitId 发现代码确实是最新的,说明经过 webpack 打包替换了, isEqual 被 tree-shaking 干掉了,构建器认为 stringify 足够。
但确实是还是报循环引用的错误,于是我使用 Chrome 的 Overrides 来修改现网源码去验证错误。
四、我利用 Chrome Overrides 热补丁修复:但又碰到了 React #185 错误
我做了个实验,把现网代码临时替换为:
js
if ((O.tool.copyObj(this.props)) !== (O.tool.copyObj(prevProps))) {
不再 stringify 和深比较。
结果报错变成:
javascript
React Error #185:
setState(...): Cannot update a component from inside the function body...
这说明:
🚨 componentDidUpdate 里的 setState 引发了新的死循环。
因为 shallow compare 仍然总是 false → 永远进入 setState → 无限循环。
然后我将代码里的 setState 给注释掉。
js
if ((O.tool.copyObj(this.props)) !== (O.tool.copyObj(prevProps))) {
// this.setState(xxx);
}
页面终于正常了!!!
五、为什么 copyObj 会制造循环?(关键根因)
项目自定义的 copyObj 代码:
bash
cloneDeepWith(obj, value => {
if (value?.$$typeof || React.isValidElement(value)) {
return ''
}
})
看似规避了 React element,
但 根本没有规避循环引用。
于是出现经典死亡链:
matlab
props → store → properties → ... → props
cloneDeep 走到循环节点 →
lodash 在无法处理循环引用 →
JSON.stringify 再次遇到循环 → 崩
六、最终修复方案设计
修复目标:
- 不使用 JSON.stringify
- 不深度比较 props
- 避免所有 setState 死循环
- 可在现网用 overrides 修热补丁
- 不破坏现有业务逻辑
🔥 我让GPT设计的临时安全方案
在 componentDidUpdate 内联一个:
- safeCopy(带 WeakMap 防环)
- isSame(浅比较)
- 严格 setState 条件判断
核心代码结构如下:
js
function safeCopy(obj) {
const seen = new WeakMap();
function clone(x) {
if (x === null || typeof x !== "object") return x;
if (seen.has(x)) return seen.get(x);
if (x.$$typeof) return "";
const result = Array.isArray(x) ? [] : {};
seen.set(x, result);
for (const key in x) {
try {
const value = x[key];
result[key] = clone(value);
} catch (err) {
}
}
return result;
}
return clone(obj);
}
function isSame(a, b) {
if (a === b) return true;
if (!a || !b) return false;
for (const k in a) {
if (a[k] !== b[k]) return false;
}
for (const k in b) {
if (a[k] !== b[k]) return false;
}
return true;
}
示例,已在现网验证有效
js
const a = safeCopy(xxx);
const b = safeCopy(xxx);
if (!isSame(a, b)) {
const nextValue = this.xxx(...);
if (this.state.value !== nextValue) {
this.setState({ value: nextValue });
}
}
这同时避免了:
- 任何形式的深比较
- 对 React element 的 clone
- 循环引用
- setState 死循环
七、总结
经过这次问题,知道了项目里还存在许多技术债,后续要继续优化。而私有云的定位比公有云比较复杂,后续要多多借助热补丁帮助定位并验证问题。
- Chrome Overrides 快速验证问题
- 代码后续重构
- 增加降级方案(后续优化)