You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React封装非React绘图库调整Chrome窗口触发标签页挂起问题

运行环境

  • Windows 10 系统
  • Chrome 92.0.4515.107 浏览器
  • React 17 版本,基于create-react-app搭建
  • 将非React应用作为JS库引入集成

问题概述

我在React项目中封装非React的编辑/绘图库,该库通过script标签而非import引入,通过window对象引用。库加载和使用均正常,但隐藏其所在父容器后调整窗口大小时,Chrome标签页会完全无响应挂起。Chrome详细日志显示如下报错:

Uncaught (in promise) undefined", source: chrome-extension://fmkadmapgofadopl...
Error: Attempting to use a disconnected port object at lOne (chrome-extension://fmkadmapgofa....

完整场景说明

我需要复用的这个封装应用是可视化编辑器,本身不是React开发的,需要大量重构才能支持import引入,因此我在index.html中通过script标签引入该库。在React代码的封装组件中,我渲染了一个div,再将该编辑器应用挂载到这个div中,符合该应用的加载要求。我通过通用JS单例对象引用该应用,通过window对象访问封装的编辑器对象,相关代码如下:

// EditorWrapper.js
import React, { Component } from 'react';
import { Button } from 'react-bootstrap';
import Editor from './Editor';

export default class EditorWrapper extends Component {
  editor = null;

  constructor(props) {
    super(props)
    this.handleSave = this.handleSave.bind(this);
  }

  // Should happen only once for lifetime of app (?)
  componentDidMount() {
    this.editor = Editor.getInstance();
    this.editor.loadEditor();
  }

  handleSave() {
    this.props.updateParent(this.editor.getData());
  }
  
  render() {
    if (this.editor !== null)
      this.editor.setData(props.getParentData());

    return (
      // Editor gets 'loaded' into this div in the Editor obj
      <div id="editor_div" style={{ display: this.props.show ? 'block' : 'none' }}>
        <Button variant="primary" onClick={() => this.handleSave()}>Save</Button>
      </div>
    );
  }
}
// Editor.js
export default class Editor {
  loaded = false;
  editor = null;
  static theInstance = null;

  constructor() {
    this.setEditor = null;
    this._scilReadyCallback = this._scilReadyCallback.bind(this);
  }

  // singleton
  static getInstance() {
    if (this.theInstance === null)
      this.theInstance = new EditorLoader();
    return this.theInstance;
  }

  getData() {
    return this.editor.app.getData();
  }

  setData(data) {
    this.editor.app.setData(data);
  }

  loadEditor = () => {
    if (!this.loaded) {
      this.loaded = true;
      window.scil.ready(this._scilReadyCallback);
    }
  }

  _scilReadyCallback() {
    window._editorPlugin.init("editor_div");  // add to DOM
    this.editor = window._editor;
  }
}

被封装的编辑器支持添加和操作各类图形元素,底层依赖JS Draw Lite实现。该库加载成本较高(会发起大量网络请求),因此我选择将其保留在DOM中,仅通过控制父div的显示/隐藏切换状态。

复现步骤

  • 运行主应用,启动时编辑器默认隐藏加载
  • 点击按钮显示编辑器
  • 在编辑器中进行绘图操作
    • 如果未进行绘图仅查看后隐藏编辑器,不会触发该问题
  • 点击按钮保存数据并隐藏编辑器
  • 手动拖动调整Chrome窗口大小

问题表现

  • Chrome标签页完全冻结无响应,页面所有交互失效
  • Chrome CPU占用从约10%飙升至30%左右,直到标签页关闭才回落
  • 可单独关闭该标签页:第一次点击标签页关闭按钮无响应,第二次点击可正常关闭
  • 标签页关闭后CPU占用恢复正常

我已开启Chrome详细日志(启动参数--enable-logging --v=1),挂起开始后每秒都会输出如下日志:

[16484:16444:0802/173052.471:ERROR:CONSOLE(0)] "Uncaught (in promise) undefined", source: chrome-extension://fmkadmapgofadopljbjfkapdkoienihi/main.html (0)
[16484:16444:0802/173052.487:INFO:CONSOLE(0)] "Error in event handler: Error: Attempting to use a disconnected port object
    at lOne (chrome-extension://fmkadmapgofadopljbjfkapdkoienihi/build/background.js:139:9)", source: chrome-extension://fmkadmapgofadopljbjfkapdkoienihi/_generated_background_page.html (0)

Chrome开发者工具会随页面一起冻结,无法进行性能 profiling 定位触发冻结的原因。我已经开始尝试排查编辑器源码,但代码量极大很难定位问题。在调整窗口前所有功能均正常:编辑器的显示隐藏、编辑操作、编辑器和封装组件的通信都没有问题,仅调整窗口时触发异常。窗口调整会触发页面重绘,CPU占用飙升和挂起的表现疑似触发了定时器相关的无限循环。


解决方案

方案合理性与React规则合规性

现有方案整体没有违反React核心规则,非React第三方库挂载到React渲染的DOM节点是通用的集成方式,仅存在两个可优化的瑕疵:

  1. 在render方法中直接调用this.editor.setData(props.getParentData()),render方法应为纯函数,不允许存在副作用操作,可能导致非预期的重复执行。
  2. 用display: none隐藏容器的方式不适用于绘图类库,是触发本次卡死的核心诱因。

更合适的封装方式

推荐使用React Portal挂载编辑器容器,完全隔离编辑器DOM和React的虚拟DOM更新逻辑,避免React重渲染意外修改编辑器内部节点。

根因与修复方案

根因:绘图类库普遍会绑定window的resize事件,在容器设置display: none时获取容器宽高会返回0,旧版本绘图库没有做边界校验,会触发重绘逻辑的无限循环,再叠加你安装的旧版本React DevTools扩展对不可见Canvas节点的状态监听,最终导致页面卡死,你日志中的报错全部来自React DevTools扩展。

修复步骤:

  1. 先禁用React DevTools扩展验证问题,可临时排除扩展的干扰
  2. 替换显隐控制逻辑:不要用display: none,改用visibility: hidden配合绝对定位移出可视区域的方式,容器宽高会正常保留,不会触发绘图库的边界异常
  3. 新增resize监听的开关逻辑:隐藏编辑器前先解绑绘图库的resize事件,显示时重新绑定,若编辑器未暴露对应接口,可手动拦截window的resize事件到编辑器的分发逻辑
  4. 调整副作用位置:把render方法中的setData调用移到componentDidUpdate生命周期中,避免重复执行:
componentDidUpdate(prevProps) {
  if (this.editor !== null && prevProps.getParentData() !== this.props.getParentData()) {
    this.editor.setData(this.props.getParentData());
  }
}

补充排查方法

如果需要定位具体触发循环的代码,可在Chrome设置中开启「网站隔离」功能,通过Chrome任务管理器单独定位该标签页的资源占用,也可以用Puppeteer无头模式访问页面,触发resize后自动导出性能日志,无需打开前端开发者工具。

内容的提问来源于stack exchange,提问作者Hawkeye Parker

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.09 14:27:03