React 项目诡异白屏事故复盘:JSON.stringify、循环引用、setState 死循环,一个都没跑

一、背景:某核心模块突然白屏

前两周某私有云客户反馈一个关键业务页面 突然白屏,刷新浏览器会好,但是经常复现,极其影响用户操作和体验。

由于是私有云,并且我公有云复现不出来,于是找到用户远程看问题。

打开控制台一看,迎面就是一串熟悉又危险的错误:

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 打包替换了, isEqualtree-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 再次遇到循环 → 崩


六、最终修复方案设计

修复目标

  1. 不使用 JSON.stringify
  2. 不深度比较 props
  3. 避免所有 setState 死循环
  4. 可在现网用 overrides 修热补丁
  5. 不破坏现有业务逻辑

🔥 我让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 死循环

七、总结

经过这次问题,知道了项目里还存在许多技术债,后续要继续优化。而私有云的定位比公有云比较复杂,后续要多多借助热补丁帮助定位并验证问题。

  1. Chrome Overrides 快速验证问题
  2. 代码后续重构
  3. 增加降级方案(后续优化)
相关推荐
Danny_FD1 小时前
使用Highcharts创建3D环形图
前端·echarts
我的div丢了肿么办1 小时前
js中async和await 的详细讲解
前端·javascript·vue.js
程序员小寒2 小时前
前端性能优化之CSS篇
前端·css·性能优化
种时光的人2 小时前
关于人人开源框架renren-fast-vue前端npm install安装报错的问题解决方法
前端·vue.js·npm
Z***25802 小时前
React增强现实案例
前端·react.js·ar
IT_陈寒3 小时前
SpringBoot 3.2 性能优化全攻略:7个让你的应用提速50%的关键技巧
前端·人工智能·后端
普通码农3 小时前
Vue-Konva 使用(缩放 / 还原 / 拖动) 示例
前端·javascript·vue.js
renxhui3 小时前
Flutter 布局 ↔ Android XML 布局 对照表(含常用属性)
前端
俺叫啥好嘞4 小时前
日志输出配置
java·服务器·前端