Files
wukuang/perf/memory-leak-and-heavy-computation-audit-2026-04-27.md
T
2026-05-23 14:05:22 +08:00

201 lines
56 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 内存泄漏与大内存/大计算风险审计
审计日期:2026-04-27
审计范围:`apps/lcdp/src/**``packages/**``docs/**`、根目录配置与现有 `perf/**` 文档。排查方式为静态代码扫描加重点文件复核,重点关注全局监听、路由守卫、事件总线、定时器、observer、第三方实例、Blob URL、长连接/流式请求、无限缓存、全量拉取、深拷贝、深度监听、递归和批量计算。
执行方式:主线程扫描与复核 + 4 个并行 agent 分区审计。分区覆盖 `apps/lcdp``packages/business-components``packages/form-designer``packages/business-module``packages/core-utils`、构建脚本和既有 `perf` 性能文档。本文已合并 agent 回传结果。
风险等级说明:
| 等级 | 含义 |
| ---- | ------------------------------------------------------------------------------------ |
| 高 | 较确定会在路由切换、组件卸载、微前端重复挂载后残留;或单次操作能产生明显大内存峰值。 |
| 中高 | 泄漏或大内存风险依赖数据量/交互路径,但影响面较大。 |
| 中 | 有残留或性能风险,需要补生命周期或限流;通常不会立即导致崩溃。 |
| 低 | 边界场景风险,建议随相关模块迭代修复。 |
## 总体结论
当前项目最需要优先治理的不是单一组件 bug,而是几类长期对象缺少统一释放机制:
1. 微前端重复挂载相关:`main.ts`、路由守卫、平台 message handler、全局 bus、全局请求配置都缺少统一 disposer。
2. 第三方实例相关:ERD Editor、Monaco Editor、自定义组件渲染、文档中心 SDK、Sortable 的实例生命周期不完整。
3. 指令/组件监听相关:部分 DOM 监听、window 监听、watch、debounce、timeout 未在卸载时清理。
4. 大数据路径相关:多处 `size: 999/9999/99999`、全量缓存、深拷贝、深度 watch、`JSON.stringify` 大对象会造成明显内存峰值和主线程阻塞。
5. 构建脚本相关:多个 package 的 `build` 同时运行 `vue-tsc --build``vite build`,再叠加 `vite-plugin-dts`、sourcemap 或 postinstall 自动构建,会放大 CI 和本地 install/build 的峰值内存。
建议优先级:
1. 先修高风险全局残留:路由守卫、postMessage、DataModelView、Monaco、自定义组件渲染、InputFormat 指令。
2. 再修卸载后异步任务:search input、plugin select、FieldDialog、UseModelAuth、Preview debounce、DocLib。
3. 最后做大数据治理:全量拉取改分页/虚拟列表,深拷贝和深度 watch 改增量 dirty 标记,AI prompt 和 ERD 数据做裁剪或 worker 化。
4. 构建阶段峰值内存单独治理:先把并行 type-check/build 改为可配置串行,再处理 dts rollup、sourcemap 和 postinstall 自动构建。
## 高风险:确定性残留或高概率泄漏
| 位置 | 风险 | 证据 | 影响 | 建议 |
| ---------------------------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
| `apps/lcdp/src/router/navigation-guards/agentGuards.ts:4` | 高 | `initAgentRouterGuards()` 内注册 `router.afterEach`,未保存注销函数;`apps/lcdp/src/main.ts:147` 每次 render 都调用。 | 微前端反复挂载时导航守卫叠加,闭包保留 agent 注册逻辑。 | 模块级 installed guard;保存 `const remove = router.afterEach(...)`;在子应用卸载时调用。 |
| `apps/lcdp/src/feature/modeling/components/model-details/entity-model/DataModelView.vue:13` | 高 | 覆盖 `Element.prototype.attachShadow`,未恢复。 | 路由切走后全局 monkey patch 继续影响所有组件和第三方库。 | 避免全局 patch;必须 patch 时幂等保存原函数,并在 `onUnmounted` 恢复。 |
| `apps/lcdp/src/feature/modeling/components/model-details/entity-model/DataModelView.vue:299` | 高 | 主/子 ERD editor 添加 `contextmenu/click` 监听、`setInterval``MutationObserver`,未统一移除或 disconnect。 | observer/timer/listener 持有 editor、shadowRoot 和组件闭包,路由切换后残留。 | 保存 listener、timer、observer 句柄;返回主视图和卸载时统一 `removeEventListener/clearInterval/clearTimeout/disconnect`。 |
| `apps/lcdp/src/feature/page-preview/hook/UsePostMessage.ts:124` | 高 | 注册 `registerPostMessageHandle` 三个全局 handler,卸载时只清理临时数据。 | handler 持有 `previewRef/baseInfo`,页面重进会重复响应。 | `registerPostMessageHandle` 返回 unregister;或补 `unregisterPostMessageHandle(type, handler)`,卸载时注销。 |
| `packages/core-utils/src/utils/PostMessageUtil.ts:42` | 高 | `postMessageHandleMap[type] = handle` 无 unregister`openListen` 注册匿名 `message` listener 无关闭能力。 | 全局 handler 表只能覆盖不能释放;message listener 生命周期不可控。 | 补 unregister 和 `closeListen`;监听函数用具名引用;仅在 handler/asyncMap 非空时保持监听。 |
| `packages/business-components/src/components/common/monaco-editor/MonacoEditor.vue:64` | 高 | 创建 editor 后没有 `onBeforeUnmount``pluginRuntime.dispose()` 未调用。`Plugins.ts:70` 的 dispose 逻辑只有其内部 effectScope stop 后才会执行。 | Monaco editor、model、监听、CodeLens provider、resize/key listeners 可能跨组件残留。 | `onBeforeUnmount(() => pluginRuntime.dispose())`,并在 runtime dispose 中 `editor.dispose()`、清理 editor listeners。 |
| `packages/business-components/src/components/common/monaco-editor/EditorWorker.ts:33` | 高 | 为 worker loader 创建 `blobUrl` 后直接缓存 worker,未 `URL.revokeObjectURL(blobUrl)`,也未提供 worker terminate/clear API。 | Monaco 多语言 worker 常驻是可接受的设计,但 Blob URL 与 worker 生命周期不可控;热更新、微前端重挂载或多版本加载会累积资源。 | worker 创建后 revoke blob URL;提供 `disposeMonacoWorkers()`,在子应用卸载或版本切换时 terminate 并清空 `workerCache`。 |
| `packages/form-designer/src/config/widget/CustomComponentRender.vue:119` | 高 | `renderFrameworkESM` 返回 `destroy`,调用方只保存 `componentInstance`;卸载时只尝试 `$destroy`。UMD Vue3/React 分支也没有 app/root unmount 返回。 | 自定义组件的 Vue/React 子应用、响应式 scope、DOM 和事件可能无法释放。 | 调用方保存 renderer handle,卸载调用 `handle.destroy()``renderFramework` 的 Vue3/React/Vue2 分支统一返回 `destroy`。 |
| `apps/lcdp/src/directives/InputFormatHandle.ts:27` | 高 | `focus/blur` 使用匿名 listener`watch([value, formatterValue], ...)` 无 stop,且无 `unmounted`。 | 指令卸载后 DOM 监听和 watcher 持有 input、value、formatterValue。 | handler 和 `stop` 存到 `el``unmounted` 中 remove + stop。 |
| `packages/form-designer/src/config/widget/CustomMobileNumber.vue:264` | 高 | watch 中创建 `ResizeObserver`,没有保存,也没有 `disconnect`。 | wrapper 变化或组件卸载后 observer 残留。 | 保存 observerwatch cleanup 或 `onBeforeUnmount` 中 disconnect。 |
| `packages/business-components/src/components/common/table/hooks/UseConfig.ts:23` | 高 | `new Sortable(el, ...)` 未保存实例,未 `destroy()``tableKey` 变化会重复初始化。 | 同一表格 tbody 上叠加拖拽监听和闭包。 | 用 `let sortable: Sortable | null`;重新初始化前和卸载时`sortable.destroy()`。 |
| `packages/form-designer/src/components/file-preview/doc-lib-preview/UseDocLib.ts:90` | 高 | `DocumentCenterSdk` 初始化后,`onBeforeUnmount``destroy()` 被注释。 | SDK iframe/overlay/message listener/会话资源可能残留。 | 需要和 SDK 方确认 `destroy` bug;至少调用 `destroyCurrentSession()`,并清理 debounce executor。 |
| `packages/form-designer/src/config/widget/file-vxe/utils/fileVxe.ts:249``:272` | 高 | `eventBus.on(BATCH_DELETE_ATTACHMENT, () => {})``eventBus.on(BATCH_DOWNLOAD_ATTACHMENT, () => {})` 使用匿名函数,`onBeforeUnmount` 只清字段变更监听。 | 每次附件表格挂载都会保留闭包和 `vxeGridRef/pageConfig/fieldtotal`。 | 将两个 handler 提升为具名函数,并在 `onBeforeUnmount` 成对 `eventBus.off(type, handler)`。 |
| `packages/business-module/src/components/script-panel/ScriptRightPanel.vue:89` | 高 | 注册的是匿名包装函数,卸载时传入 `handlerSwitch` 执行 `off`。 | on/off 函数引用不一致,监听无法移除。 | 保存 `const handleSwitchRightHeader = (...) => { ... }`,注册和移除使用同一引用。 |
| `packages/form-designer/src/config/settings/event-list/components/ActionContent.vue:247` | 高 | `eventBus.on(..., (id) => customTreeRef.value?.remove(id))` 无对应卸载清理。 | 会保留 `customTreeRef/pageConfig/jsData` 等上下文,事件页反复打开后监听叠加。 | 增加命名 handler`onBeforeUnmount``eventBus.off(type, handler)`,并清理 `setTimeout`。 |
| `apps/lcdp/src/feature/modeling/components/custom-component/group-tree-select/component/group-panel/store/group.ts:37` | 高 | 模块级 `storeCache: Map<appId, Store>` 只增不删。 | 切换多个 app 后 store 构造器和 group 数据长期驻留。 | 改单 store keyed state;或提供 `clearGroupStore(appId)`/LRU,并在离开应用时清理。 |
| `apps/lcdp/src/App.vue:20``apps/lcdp/src/AppMobile.vue:15` | 中高 | `<KeepAlive>``max/include/exclude`。 | runtime 页面、设计器、预览页长期保留组件实例、请求状态和第三方实例。 | 设置 `max`;只缓存明确需要的轻页面;重页面禁用缓存或实现淘汰策略。 |
## 中高风险:卸载后异步任务、全局注册和常驻 UI
| 位置 | 风险 | 证据 | 影响 | 建议 |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | --------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `apps/lcdp/src/main.ts:139``:150``:174` | 中高 | `requestIdleCallback``setTimeout` 未保存 handle。 | 微前端卸载后仍可能执行组件注册、语言包加载,闭包持有 app。 | 保存 idle/timeout idunmount 时 cancel;回调中判断 app 是否仍有效。 |
| `apps/lcdp/src/utils/UsePlatformPostMessageHandler.ts:7` | 中高 | 底层 `init()` 注册 window message,未接收 disposer。 | 重复初始化可能叠加平台 message listener。 | 包装为幂等单例,暴露 destroy;确认底层是否支持 stop。 |
| `apps/lcdp/src/utils/MsgPackInterceptors.ts:5` | 中高 | 每次调用注册 axios interceptors,未保存 id,未 eject。当前 main 中注释,但启用后有风险。 | 重复挂载导致拦截器叠加。 | installed guard;保存 request/response idcleanup 时 eject。 |
| `apps/lcdp/src/utils/mobileAuthUtils.ts:32` | 中 | `execAutoRefreshToken(reIns.getAxiosIns())` 无 stop/幂等保护。 | 自动刷新定时器或拦截器可能重复注册。 | 确认底层返回值;增加单例 guard 和 stop。 |
| `apps/lcdp/src/components/common/search-input/index.vue:29``:59` | 中高 | debounce timeout 未卸载清理;focus 时 window keydownblur 时移除。 | 聚焦状态下组件销毁可能残留 window listener;最后一个 timeout 仍写状态/emit。 | `onBeforeUnmount``clearTimeout(timeout)``removeEventListener`。 |
| `apps/lcdp/src/directives/ImageHandle.ts:32``:45` | 中 | 原生 img 与内部 img 使用匿名 `error/load` handler;卸载只 disconnect observer。 | 已绑定到真实 img 的 listener 不能精准移除。 | 记录 img 与 handler,卸载时 remove;移除临时 img 并清空字段。 |
| `apps/lcdp/src/components/business-common/plugin-select/hook/usePluginSelect.ts:91``:105``:197``:228``:265` | 中 | 请求完成后继续写 state;多处 timeout 未清理。 | 卸载后异步回调操作 DOM/ref;请求结果过期仍覆盖新状态。 | disposed flag 或 request id;保存 timeout id 数组并在卸载清理。 |
| `apps/lcdp/src/components/business-common/field-dialog/FieldDialog.vue:120` | 中 | 请求完成后写 `list/total/status`,关闭弹窗只重置 `isInit`。 | 弹窗关闭/卸载后旧请求仍写状态。 | 请求版本号或 AbortController;关闭/卸载时忽略旧响应。 |
| `packages/form-designer/src/components/btn-action/components/model-import-export/components/file/File.vue:22` | 中 | 上传态 `setInterval` 只在 `state.type` 变化时清理,没有卸载清理。 | 弹窗或组件在上传态关闭时 interval 留存,继续写 `progress`。 | 引入 `onUnmounted(() => clearInterval(timer))`;重新进入 upload 前先清旧 interval。 |
| `packages/core-utils/src/hooks/UseBatchDownload.ts:248` | 中 | 每次调度都向外部 `AbortSignal` 添加 `abort` listener,未在调度结束后移除。 | 长生命周期 signal 多次下载会积累 listenerlistener 闭包持有 controller。 | 使用 `{ once: true }`,或保存 handler 并在 scheduler resolve/finally 后 remove。 |
| `apps/lcdp/src/feature/modeling/components/model-details/entity-model/CurrentModeInfo.vue:116``:145` | 中 | lodash debounce 未 canceltooltip timeout 未清理。 | pending debounce 可能在卸载后重新绑定 DOM 监听。 | `deCostFormMounted.cancel()``clearTimeout(time)`,并加 disposed guard。 |
| `apps/lcdp/src/feature/page-preview/Preview.vue:71``PreviewMobile.vue:94` | 中 | lodash debounce 未 cancel;多个 deep watch 改 `key` 触发重挂载。 | 失活/卸载后 pending 路由回调写状态;深 watch 扩大重渲染面。 | `onDeactivated/onUnmounted` cancelwatch 标量数组而非 deep object。 |
| `apps/lcdp/src/feature/modeling/components/model-details/auth-control/UseModelAuth.ts:34``:66` | 中 | debounced save/fetch 未 cancel;请求无 stale 判断。 | 权限页卸载后延迟保存/拉取继续持有 `authInfo`。 | 保存 debounced fetch 引用;卸载时 cancel/flush;请求加 stale token。 |
| `apps/lcdp/src/feature/modeling/components/list/business-model/BusinessModelList.vue:222``page-model/PageModelList.vue:182``mixed-list/MixedList.vue:191` | 中高 | `showMessage({ duration: 0, append: h(... onClick ...) })`。 | 全局常驻 message 闭包持有组件方法和列表数据。 | 保存 message handle,卸载或列表刷新时 close;设置有限 duration;点击时按 id 查询当前数据。 |
| `packages/form-designer/src/config/widget/list-vxe/composables/fetch.ts:196` | 中 | `getTableData` 是 lodash debounce;文件内未见 `onUnmounted`/`onScopeDispose``.cancel()`。 | 列表卸载后 pending debounce 仍可能触发请求或写入已失效的 ref;已有 abort 只在函数执行后才生效。 | 作用域销毁时 `getTableData.cancel()``abortPendingRequest()`。 |
| `packages/business-components/src/utils/VirtualTooltip.ts:5` | 中 | 全局 tooltip container/vnode/currentEl/timer 无销毁 API。 | 长生命周期单例可接受,但当前元素引用和 popper listener 可能滞留到下一次显示。 | 提供 `destroyGlobalVirtualTooltip()`;隐藏时清空 virtualRef/currentEl 并清理 timer。 |
| `packages/business-components/src/composables/VirtualPopover.tsx:70``:222` | 低到中 | unmount 时 off 了 mitt,但 debounce open/close 未 cancel。 | pending debounce 可能在根组件卸载后改全局 state。 | `onUnmounted` 中 cancel `debounceOpenPopover/debounceClosePopover`。 |
## 大内存和大计算热点
| 位置 | 风险 | 证据 | 影响 | 建议 |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| `apps/lcdp/src/feature/modeling/components/model-details/entity-model/DataModelView.vue:87``:142``:377` | 高 | 每 200ms 递归 `querySelectorAll('*')` 扫 Shadow DOM 并重新注入样式;ERD 数据整体 `JSON.stringify`。 | 大 ER 图下 CPU 高、短期对象多,可能触发频繁 GC。 | 优先使用 ERD API/配置隐藏工具栏;observer 只监听目标节点;大数据转换和序列化移 worker 或限量。 |
| `apps/lcdp/src/service/BillTransferService.ts:67` | 高 | 流式 AI 响应 `accumulatedContent += value``cancelFn` 到 onDone 才随 resolve 返回。 | 调用方请求中途拿不到取消句柄;页面卸载后流继续累积大字符串。 | 立即返回 `{ promise, cancelFn }` 或接收 `AbortSignal`;限制累计长度;取消/失败时清空缓冲。 |
| `apps/lcdp/src/feature/bill-transfer/hooks/useFieldMappingAI.ts:117``:121``:147``:201` | 中高 | `flatTableData` 有数据就不重建;`cloneDeep(tableData)`Map 持有行对象;`JSON.stringify(pairs)`。 | 表格变更后缓存不失效;大映射序列化和 Map 引用旧行。 | 按 source/target/tableData 版本重建;完成后清空 Map;AI 入参分批或裁剪字段。 |
| `apps/lcdp/src/service/ModelingService.ts:67` | 中高 | 导出模型 `JSON.stringify(res, null, 2)`,再 Blob 和 `URL.createObjectURL`,未 revoke。 | 大模型导出有字符串+Blob 双份内存峰值;Blob URL 泄漏。 | 复用 `downloadBlob` 或点击后 `URL.revokeObjectURL(url)`;大数据后端直接返回 blob/流。 |
| `apps/lcdp/src/service/ApplicationService.ts:95``:203` | 中高 | `size: 99999`;递归转换中反复复制 path 数组。 | 大租户业务/应用树会带来网络、内存和 CPU 压力。 | 后端分页/懒加载;限制 size;路径用 parentId 或共享数组迭代构建。 |
| `apps/lcdp/src/components/business-common/business-dropdown/index.vue:28` | 中 | `size: 99999` 拉全量,再前端 slice 展示。 | 大业务分类时前端持有全量列表。 | 分页/远程搜索;输入 debounce;不要保留全量。 |
| `apps/lcdp/src/components/business-common/field-dialog/FieldDialog.vue:125``:177` | 中 | 主列表和子节点都 `size: 999`,展开时 map/filter。 | 引用字段树懒加载可能多次拉大列表和复制数组。 | 后端分页/限制层级;按 referenceId 缓存;只加载必要字段。 |
| `apps/lcdp/src/components/common/search-select/index.vue:69` | 低到中 | `watch(searchOptions, ..., { deep: true })`。 | 选项多时深度遍历成本高。 | 改浅 watch 或赋值后直接同步 `resShowOptions`。 |
| `apps/lcdp/src/components/business-common/app-select/index.vue:34` | 低 | 打开弹窗 `cloneDeep(list)`。 | list 较大时开窗产生完整副本。 | 只复制会被修改字段;用浅拷贝加按需 clone。 |
| `apps/lcdp/src/feature/modeling/components/model-details/entity-model/hooks/UseTreeLogic.ts:229``:263``:387``:632` | 中高 | 多处 `cloneDeep` 整树/对象;深度 watch `currentSelectedData``treeEvent.editNode(cloneDeep(...))`。 | 字段属性编辑会频繁触发树节点重写和深拷贝。 | 只 watch 模型基础字段;字段表单变更不要驱动树;dirty/version 标记替代深度 watch。 |
| `apps/lcdp/src/feature/modeling/components/model-details/entity-model/hooks/UseModelAndFieldChange.ts:31``Util.ts:7` | 中高 | `requestDataIsChange` 对整个 requestData 做 `isEqual`;复制模型路径多处 `cloneDeep`。 | 模型/子模型/字段操作列表越大,任意响应式变更都可能触发递归比较。 | 维护增量 dirty flag;按 operation list 判断变更;避免 computed 中全量深比较。 |
| `apps/lcdp/src/feature/modeling/components/model-details/feature-variable/hooks/useFeatureGroup.ts:225``:245``:304``:342` | 中高 | `tableListGather` 缓存全量数据;分页/搜索前 `cloneDeep` 全量列表; debounce 未 cancel。 | 数据多时内存翻倍,分页和搜索 CPU 高。 | 服务端分页/搜索;只克隆当前页;卸载清空缓存并 cancel debounce。 |
| `apps/lcdp/src/feature/print/hooks/print.ts:197``Config/utils.ts:12``:33` | 中 | computed 中反复 flatten tree;节点上挂 `parent`transform 写 parent/path。 | 大打印模型树会产生大量临时对象和父链引用。 | 数据变化时一次性构建 `_path -> node` Map;关闭弹窗清理;节点只存 parentId/path。 |
| `apps/lcdp/src/feature/modeling/components/list/business-model/BusinessModelList.vue:112``page-model/PageModelList.vue:222``mixed-list/MixedList.vue:518``:545` | 中 | 批量引用详情用 `Promise.all(cannotDelete*.map(...))`。 | 大量不可删项会并发打满请求,并一次性保存全部详情。 | 限制并发;弹窗打开后分页/按需请求引用详情。 |
| `apps/lcdp/src/feature/modeling/components/list/mixed-list/MixedList.vue:136``model-details/index.vue:419``business-operation/FormEdit.vue:156` | 中 | 多处 `size: 9999` 拉页面/规则/操作列表。 | 一次性大列表进入 refs/options,路由切换无 abort。 | 改后端分页/搜索型 select;弹开再加载;请求加 AbortController 或 stale id。 |
| `apps/lcdp/src/feature/modeling/components/model-details/entity-model/hooks/UseAiBatchAdd.ts:304``:322` | 中 | AI 上下文 `JSON.stringify(treeData.map(...))` 和全部字段列表;还有大对象 console。 | 大模型会生成大 prompt 字符串并阻塞主线程。 | 限制字段数量/深度,只传当前模型摘要;移除大对象 log;必要时 worker 构造 prompt。 |
| `apps/lcdp/src/feature/deployment-package/components/log/index.vue:23``:39` | 中低 | 一次性 `logList.value = res`,全量 `v-for` 渲染日志。 | 大安装日志占内存并卡渲染。 | 分页/虚拟列表;限制最大展示行数并提供完整日志下载。 |
| `apps/lcdp/src/views/demo/vxe-table/index.vue:57``grid.vue:240``tree-v2/index.vue:77` | 中 | demo 可生成 5w 行 × 120 列,树 demo 参数组合可生成极大节点数。 | 若路由可访问,会冻结主线程和内存暴涨。 | 加总量上限;大数据生成分片/worker;非虚拟表不要提供极端数据。 |
| `apps/lcdp/src/views/RuleRuntimeTest.vue:62` | 中低 | `new RuntimeRuleEventCenter(...)` 未见卸载销毁。 | 如果规则中心内部有事件或缓存,实例可能跨页面滞留。 | 确认类是否有 destroy/off/dispose;卸载调用。 |
| `packages/form-designer/src/config/widget/list-vxe/preview/core-table/VxeTablePlus.vue:239` | 中 | 纵向虚拟滚动已开启,但 `virtualXConfig.enabled=false`。 | 37+ 列宽表仍会让所有列参与渲染,横向大列数下 DOM/布局成本高。 | 在 fixed 列场景回归后开启横向虚拟滚动,或按列数阈值启用。 |
| `packages/form-designer/src/config/widget/table/FieldViewMap.ts:36` | 中高 | 绑定模型的选择字段仍走 `Tag/TextCollapseTag/LightText` 等组件渲染。 | 大列表中每单元格生成组件/VNode,已优化的纯文本路径不能覆盖该场景。 | 列表模式下为绑定模型选择字段提供纯文本 formatter,只有交互态再挂重组件。 |
| `packages/form-designer/src/config/widget/list-vxe/composables/useCacheComputed.ts:5` | 中 | `stableStringify` 对 watcher 值排序 key 并 `JSON.stringify`;结果还用 `isEqual` 比较。 | 大 schema/columns 变化时深遍历、字符串分配和深比较叠加。 | 用明确依赖的 shallow watch/computed 替代;逐步移除大对象 stringify。 |
| `packages/form-designer/src/config/widget/list-vxe/preview/core-table/useMergeCells.ts:117` | 中 | 每次为字段路径创建 `values/truthy/sigs` 数组,对对象值 `safeStringify`。 | O(rows \* paths) 预计算和字符串分配会在大表格下形成内存峰值。 | 先判断是否存在合并/分组列;只预计算可合并字段。 |
| `packages/form-designer/src/config/widget/list-vxe/preview/ListVxePreview.vue:632``:637` | 中 | 暴露的 `getTableData/addTableData` 对整页表格数据 `deepCloneObj`。 | 外部频繁读取/新增时复制整页对象图。 | 读接口返回只读引用或浅拷贝;只在外部可变写入边界 clone。 |
| `packages/form-designer/src/config/widget/list-vxe/index.ts:2` | 中 | 仍导出旧 `VxeTable.vue`,外部 import 可能绕过 `VxeTablePlus`。 | 旧实现的 `useCacheComputed` 与渲染路径仍可能被业务使用。 | 标记旧导出废弃或指向 Plus;补兼容测试。 |
| `packages/form-designer/src/components/settings-operation/index.vue:101` | 中 | 默认 `size: 9999` 获取所有操作。 | 大模型操作多时,一次性进入 `optionList` 并渲染。 | 改远程搜索/分页 select;仅弹窗打开时加载当前页。 |
| `packages/form-designer/src/hooks/useLoadPage.ts:149``:565` | 中 | 页面加载两处字段列表请求 `size: 999`。 | 多页面/多模型初始化时会把字段元数据批量拉入 store。 | 字段元数据按组件/引用路径懒加载;通用 field key 服务加缓存上限和按页清理。 |
| `packages/business-module/src/components/script-panel/Event.ts:60` | 中 | 脚本面板加载页面列表 `size: 999`,并在内存中构建树。 | 大模型多页面场景下脚本面板打开即产生大列表和树对象。 | 改按关键字/节点懒加载;树节点展开再请求。 |
| `packages/business-components/src/components/business/reference-data-select/FieldDialog.vue:188``hooks/useReferenceDataSource.ts:85``hooks/useFeatureDomainDataSource.ts:157``hooks/useFilterDataSource.ts:75` | 中 | 引用字段选择多处子节点加载 `size: 999`。 | 深引用模型下每次展开都可能拉完整字段列表,并映射生成路径字符串。 | 按层分页、限制最大展开数;相同 referenceId 加 TTL/LRU 缓存。 |
| `packages/form-designer/src/widget-mobile/composables/list-fetch/useMobileFetch.ts:249``:260` | 中 | deep watch 内用 `JSON.stringify` 比较 filter/order。 | 条件树变大时每次变更都会整树序列化,触发主线程阻塞和临时字符串内存。 | 在 store 内维护版本号/hash,或按字段浅比较。 |
## packages 目录的长期缓存与工具风险
| 位置 | 风险 | 证据 | 影响 | 建议 |
| --------------------------------------------------------------------------------------------------------------------------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `packages/form-designer/src/service/FilterService.ts:27` | 中高 | `fieldOptionsCache``resolverWaits` 为模块级 Map;成功和失败结果都写入缓存,未见清理 API。 | 访问大量 model 后字段选项长期驻留。 | 增加 `clearFieldOptionsCache(modelId?)`、TTL/LRU;页面卸载或切 app 时清理。 |
| `packages/form-designer/src/widget-mobile/utils/field-mapping-cache.ts:11``:15` | 中 | `fieldMappingCache``fieldSchemaCache` 按 modelId 全局缓存完整字段映射和字段 schema。 | 移动端多模型页面切换后不会自动释放字段配置对象。 | 页面卸载或模型切换时调用 `clearFieldMappingCache(modelId)`;加容量限制。 |
| `packages/form-designer/src/service/FieldKeyService.ts:20``:40` | 中 | `fieldTreeParamsList/fetchLocal/fieldTreeTimer``model_app` key 保留;请求 flush 后只清数组,不删除 key 和 timer 引用。 | 多 model/app 组合会留下大量空数组、空队列和 timer key。 | 请求结束后删除空 key;失败也清理;必要时加 `clearFieldTreeBatchCache(tagKey?)`。 |
| `packages/business-components/src/service/common/BatchFetchLocalService.ts:11``BatchParamsService.ts:14``BatchParamsService.ts:111` | 中 | 模块级请求聚合缓存,返回前 `deepCloneObj`10s 后清理成功响应;`ApiLocalMap[primaryKey]` 不删除空容器。 | 短时间大量 key 会积压响应和 promise;不同 params 会留下大量空 Map;深拷贝放大 CPU/内存峰值。 | 增加最大 key 数、总大小限制;response/request 为空时删除 primaryKey;失败/超时清理 request;大响应避免深拷贝。 |
| `packages/business-module/src/stores/Page.ts:22` | 中 | `pageStoreMap` 只增不删,`resetPageDataCode` 只重置数据,不 dispose store。 | 脚本面板多 pageId 切换会保留 Pinia store 和响应式对象。 | 增加 `deletePageStore(pageId)`:调用 `$dispose()` 并从 Map 删除;面板卸载时调用。 |
| `packages/business-components/src/utils/InterceptorUtils.ts:43` | 中 | `CACHE_INFO_DATA` localStorage 对象只增不删。 | 长期使用多个页面/模型后本地缓存膨胀。 | 给缓存项加版本/时间戳/LRU;应用切换时清理旧项。 |
| `packages/form-designer/src/utils/LoadUmd.ts:6``ImportScript.ts:19` | 中 | UMD load promise 全局缓存;script 成功后不移除;`loadSourceWithCache` 的 then 中没有 return。 | 动态组件越多,script/window 全局和 promise 缓存越多。 | 按组件版本设计 LRU;提供 unload 能力;修复 `return get(window, umdPathKey)`。 |
| `packages/form-designer/src/utils/LoadCustomResource.ts:172``:185` | 中 | `cssLinkMap` 全局缓存并向 head 追加 link,无移除。 | 动态加载大量组件样式后 CSS 常驻。 | 明确这是进程级缓存;或按组件引用计数清理。 |
| `packages/lowcode-create/src/logics/SandBox.ts:57``:140` | 中 | watcher 在 `effectScope` 中,`CodeProtocol.destroy()` 会 stop;但 `setFieldStyle` 的 3s timeout 未保存。 | 表单卸载后 timeout 仍可能查询/写 DOM。 | 保存 timeout id`stopEffect` 时 clearDOM 操作前检查 disposed。 |
| `packages/form-designer/src/config/widget/list-vxe/preview/core-table/VxeTable.vue:449``VxeTablePlus.vue:462` | 低到中 | ResizeObserver 有 disconnect,但 debounce/内部 setTimeout 未 cancel。 | 卸载边界下仍可能执行一次 recalculate 相关回调。 | 卸载时 cancel debouncesetTimeout 前后判断 mounted。 |
| `packages/business-components/src/components/business/upload/UseUploadModel.ts:14` | 低 | Blob URL 在 scope dispose 时统一 revoke。 | 删除单个 raw 文件时 URL 会保留到组件卸载。 | 文件移除时同步 revoke 对应 URL。 |
## 构建脚本内存峰值风险
| 位置 | 风险 | 证据 | 影响 | 建议 |
| ------------------------------------------------------------------------------------ | ---- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `packages/form-designer/package.json:22``packages/business-module/package.json:22` | 高 | `build` 使用 `run-p type-check "build-only"` 并行运行 `vue-tsc --build``vite build`。 | 两个进程同时加载 TS/Vue/Rollup 依赖图,CI 或本地低内存机器峰值接近翻倍。 | CI 改串行阶段;或拆 job;必要时为 build/type-check 单独设置 `NODE_OPTIONS=--max-old-space-size=...`。 |
| `packages/widget-pc/package.json:13` | 高 | `pnpm test && run-p type-check "build-only"`。 | 测试结束后仍并行类型检查和构建,内存峰值高;失败重试成本也高。 | 测试、类型检查、构建分阶段执行;发布流水线再组合。 |
| `packages/widget-pc/vite.config.ts:20``:23` | 中 | `vite-plugin-dts` 启用 `rollupTypes: true`。 | dts rollup 会构建类型图,和 Vite/Rollup 同进程叠加内存。 | 非发布构建关闭 `rollupTypes`;类型声明单独命令生成。 |
| `packages/lowcode-create/vite.config.ts:21``:55` | 中 | lib build 同时开启 `sourcemap: true``dts()`。 | sourcemap 与类型生成在同一构建进程中增加内存占用。 | 发布构建才开 sourcemap;类型生成拆为独立步骤。 |
| `package.json:25` | 中 | `postinstall` 自动执行 `pnpm run build:core`。 | install 阶段触发重型 Vite 构建,CI 缓存失效或本地安装时容易出现不可预期峰值。 | 移出 postinstall,改为显式 `bootstrap/build` 步骤。 |
## 已检查但暂未列为主要问题
| 位置 | 结论 |
| ------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `apps/lcdp/src/directives/TextHoverHandle.ts``apps/lcdp/src/directives/vTooltip.ts` | DOM listener 有成对 remove,主要问题是全局 tooltip 单例本身缺少 destroy。 |
| `packages/form-designer/src/hooks/useResizable.ts` | mousemove/mouseup 和 ResizeObserver 在卸载时清理,当前实现基本完整。 |
| `packages/form-designer/src/config/widget/list-vxe/preview/core-table/*` | ResizeObserver 有 disconnect;残余风险在 debounce/setTimeout cancel。 |
| `packages/business-components/src/components/business/upload/UseUploadModel.ts` | 已在 `onScopeDispose` revoke Object URL。 |
| `perf/list-vxe-performance-analysis.md` | 已有 list-vxe 渲染卡顿分析,结论可继续沿用:大分页下主要瓶颈是 vxe-grid DOM 渲染,而非请求。 |
## 建议落地任务
### P0:先止住确定性泄漏
1. 给微前端生命周期加统一 cleanup registry:路由守卫、idle/timeout、平台 message、AI 初始化、theme adapter、axios interceptors 都登记 disposer。
2. 修复 `DataModelView.vue`:恢复 `attachShadow`、清理 editor listener/timer/observer,避免 200ms 轮询扫全 Shadow DOM。
3. 修复 `MonacoEditor.vue` 和 plugin runtime:组件卸载时 dispose editor 和所有 provider/listener。
4. 修复 `EditorWorker.ts`revoke Blob URL,并提供 worker cache 的 terminate/clear。
5. 修复自定义组件渲染:`renderFramework/renderFrameworkESM/renderDebugFrameworkESM` 返回统一 handle,调用方卸载时调用 `destroy()`
6. 修复 `InputFormatHandle.ts``CustomMobileNumber.vue``UseConfig.ts` Sortable、`UsePostMessage.ts``fileVxe.ts``ScriptRightPanel.vue``ActionContent.vue` 的生命周期缺口。
### P1:控制异步与常驻 UI
1. 所有请求型组件增加 stale request id 或 AbortController。
2. lodash debounce/throttle 统一在卸载 cancel。
3. `showMessage({ duration: 0 })` 保存 handle,组件卸载时关闭。
4. `getAICodeService` 改为可立即取消,并限制流式内容累计大小。
5. 对通用 eventBus/mitt 封装开发期泄漏检测:同一组件重复注册、匿名 handler、on/off 引用不一致时输出告警。
### P2:降低大数据内存和 CPU 峰值
1. 清理 `size: 999/9999/99999`,改分页、懒加载、远程搜索或虚拟列表。
2. 深拷贝和深度 watch 改为 dirty flag、版本号、局部浅拷贝。
3. 大 prompt、大 ERD JSON、大导出 JSON 放到 worker 或后端生成。
4. 模块级 Map 缓存增加 TTL/LRU 和显式 clear API。
5. list-vxe 继续验证横向虚拟滚动、绑定模型字段纯文本渲染、`useCacheComputed` 和 merge-cell 预计算优化。
### P3:降低构建阶段峰值内存
1. `form-designer``business-module``widget-pc` 的 build 改为 test/type-check/build 分阶段串行。
2. dts rollup、sourcemap 和 Vite build 拆成发布专用步骤。
3. 移除根 `postinstall` 的自动重构建,改显式 bootstrap。
## 验证建议
1. 微前端重复挂载测试:同一页面 mount/unmount 20 次,检查路由守卫数量、message listener、axios interceptors 是否增长。
2. Chrome Memory heap snapshot:进入/退出建模详情、ERD 视图、页面预览、Monaco 编辑器、自定义组件渲染页,比较 detached DOM、ResizeObserver、MutationObserver、Monaco model、Sortable 实例数量。
3. 大数据压测:业务分类、字段选择、应用树、特征变量、打印模型树用 1k/5k/10k 数据量测试内存峰值和 long task。
4. AI 流式请求测试:发起后立即切路由,确认请求被 abort,`accumulatedContent` 不再增长。
5. 增加开发期诊断:包装 eventBus/router/axios/registerPostMessageHandle,开发模式输出当前 listener/interceptor/handler 计数。
6. list-vxe 当前主路径已使用 `VxeTablePlus`,建议重新补跑 `perf/list-vxe-performance-test.md` 的基线数据,避免后续继续引用旧实现测试结果。
## Agent 分工记录
| 分工 | 覆盖范围 | 结果 |
| ------- | ----------------------------------------------------- | ------------------------------------------------------------------------------- |
| 主线程 | 全仓静态扫描、重点文件复核、文档汇总 | 已合并所有确认项。 |
| Agent A | `apps/lcdp/src` 全局生命周期、页面预览、建模详情 | 发现路由守卫、postMessage、ERD editor、指令、全量请求等问题。 |
| Agent B | `packages/business-components``packages/core-utils` | 发现 Monaco、PostMessageUtil、Batch service、Tooltip、UseBatchDownload 等问题。 |
| Agent C | `packages/form-designer` 运行时、页面加载、动态组件 | 发现自定义组件渲染、DocLib、FilterService、LoadUmd、list/tree 字段加载等问题。 |
| Agent D | `list-vxe`、脚本面板、构建脚本 | 补充 list-vxe 热点、匿名 eventBus 泄漏、脚本面板 store 缓存、构建峰值内存风险。 |