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节点是通用的集成方式,仅存在两个可优化的瑕疵:
- 在render方法中直接调用
this.editor.setData(props.getParentData()),render方法应为纯函数,不允许存在副作用操作,可能导致非预期的重复执行。 - 用
display: none隐藏容器的方式不适用于绘图类库,是触发本次卡死的核心诱因。
更合适的封装方式
推荐使用React Portal挂载编辑器容器,完全隔离编辑器DOM和React的虚拟DOM更新逻辑,避免React重渲染意外修改编辑器内部节点。
根因与修复方案
根因:绘图类库普遍会绑定window的resize事件,在容器设置display: none时获取容器宽高会返回0,旧版本绘图库没有做边界校验,会触发重绘逻辑的无限循环,再叠加你安装的旧版本React DevTools扩展对不可见Canvas节点的状态监听,最终导致页面卡死,你日志中的报错全部来自React DevTools扩展。
修复步骤:
- 先禁用React DevTools扩展验证问题,可临时排除扩展的干扰
- 替换显隐控制逻辑:不要用
display: none,改用visibility: hidden配合绝对定位移出可视区域的方式,容器宽高会正常保留,不会触发绘图库的边界异常 - 新增resize监听的开关逻辑:隐藏编辑器前先解绑绘图库的resize事件,显示时重新绑定,若编辑器未暴露对应接口,可手动拦截window的resize事件到编辑器的分发逻辑
- 调整副作用位置:把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

