Files
next-tjwater-frontend/docs/rive-persona-incident.md
T
jiang 2d8506fea0
Generic Container CI/CD / test-build-publish (push) Successful in 2m9s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 2m39s
feat: add project-aware agent workspace
2026-10-22 18:52:18 +08:00

92 lines
5.6 KiB
Markdown

# Rive Persona 内存问题排查记录
日期:2026-07-21
## 背景
`next-webgis` 中的 Obsidian Persona 使用 Rive WebGL2 渲染。迁移到
`next-tjwater-frontend` 的 Vite、React 运行环境后,页面加载 Persona 时出现浏览器
`Out of Memory`,同时需要保留原有羽化灰黑球体的视觉效果。
本次排障验证使用 `@rive-app/react-webgl2@4.29.3`,依赖声明保留 4.x 兼容版本范围。
资源仍使用:
```text
https://ejiidnob33g9ap1r.public.blob.vercel-storage.com/obsidian-2.0.riv
```
## 已证实的触发条件
在同一 Chromium 环境下进行了对照测试。以下内存值是排障时的单次观测范围,
用于判断增长趋势,不应视为跨设备性能基准。
| 场景 | 结果 |
| ------------------------------------------------- | ----------------------------------------------- |
| Vite、WebGL2、绑定 Rive View Model | 渲染进程快速增长到约 4.6 GB,随后失去响应或崩溃 |
| Vite、阻止 `.riv` 加载 | 约 29 MB 至 61 MB,保持稳定 |
| Vite、Canvas、不绑定 View Model | 内存稳定,但渲染边缘和质感不符合原设计 |
| Vite、Canvas、调用 `bindViewModelInstance` | 再次出现持续增长 |
| Vite、WebGL2 4.29.3、调用 `bindViewModelInstance` | 再次出现持续增长 |
| Vite、WebGL2 4.29.3、不绑定 View Model | 约 71 MB 至 116 MB,最终约 78 MB |
| `next-webgis` 原实现 | 约 165 MB 至 221 MB,未出现持续增长 |
可以确认:在迁移后的 Vite 应用中,`bindViewModelInstance` 是泄漏的稳定触发点。
`useViewModelInstance` 内部也会执行该绑定,因此仅替换颜色 hook 不能解决问题。
不能仅凭现有证据断言缺陷完全属于 Vite、React 或 Rive 某一方。`next-webgis` 的同一
资源和完整绑定能够稳定运行,说明这是新运行环境与 Rive View Model 绑定生命周期的
兼容问题。若要定位到 Rive 内部对象,需要进一步制作最小复现并进行运行时级堆分析。
## 为什么不是 TypeScript 7
TypeScript 在该项目中只通过 `tsc --noEmit` 做静态检查。浏览器实际执行的是 Vite 和
SWC 转换后的 JavaScript。泄漏可以通过是否调用 Rive View Model 绑定稳定开关,因此
与 TypeScript 类型检查器无关。
## 最终修改
1. 保留 Rive WebGL2、Obsidian 资源和 `default` 状态机。
2. 移除 `useViewModel`、`useViewModelInstance`、`useViewModelInstanceColor` 及所有
`bindViewModelInstance` 调用。
3. 使用 CSS `brightness` 和 `contrast` 保持浅色界面中的灰黑视觉,不再监听并不存在的
应用深色主题。
4. Persona 仅在元素进入视口且页面可见时挂载,避免桌面和移动响应式节点同时初始化。
5. 首次初始化延迟到下一帧,使 React Strict Mode 的探测挂载可以在创建 Rive 实例前取消。
6. 保留懒加载和加载失败占位,但移除实验阶段的多皮肤、重复状态推导和无用事件透传。
7. 移除 Persona 环境变量开关,动画由组件生命周期直接管理。
## 回归保护
- `src/shared/ai-elements/persona.test.tsx` 验证加载的是 Obsidian 和 `default` 状态机,
同时保证不会调用 `bindViewModelInstance`。
- `src/app/app.e2e.ts` 验证页面只发出一次 `.riv` 请求。
- 桌面 1440 x 900 和移动端 375 x 812 均验证无横向溢出。
验证命令:
```bash
pnpm typecheck
pnpm lint
pnpm test
pnpm test:browser
pnpm build
```
## 后续修改约束
- 在独立最小复现和持续内存采样通过前,不要重新引入 Rive View Model 绑定。
- 不要用 CSS 隐藏两个已挂载的 Persona,响应式副本必须通过可见性挂载保持单实例。
- 不要移除 Strict Mode 延迟初始化,除非重新验证开发和生产生命周期。
- 更新 Rive 版本时,至少验证单次 `.riv` 请求、状态机切换、页面切换后的实例清理和
30 秒以上的浏览器进程内存趋势。
## 2026-10-21 原版恢复后的现状
Agent 入口使用基于原版 Obsidian 定制的冰水蓝资源。继续保留 Strict Mode 延迟初始化、按可见性挂载和禁止 View Model 绑定;配色直接写入资源,不应用旧版的 brightness / contrast 滤镜。`persona-resource.ts` 在页面内存与浏览器 Cache Storage 中缓存动画字节,按内容版本更新,不共享 Rive 实例或 View Model。
业务离线由本地 `obsidian-water-v1.riv` 的 `asleep` 状态播放内部缓慢水纹并关闭外围光圈,在线/离线切换保留同一实例。减少动态效果、加载和失败时显示从当前资源渲染的 `obsidian-water-idle.png` 或无光圈的 `obsidian-water-offline.png`。隐藏和减少动态效果会卸载实例,不通过 CSS 同时隐藏多个仍运行的 Persona。资源改造见 [Agent 动画资源说明](agent-persona-resource.md)。`hover` 输入由外层交互控制,Rive 自带的 DOM 监听关闭,避免和原生入口按钮及触摸操作重复响应。
回归验证已覆盖原始字节缓存、失败后的重试、无 View Model 绑定、原生状态及 hover 切换、真实 canvas 动画和桌面/手机单实例。
独立原版 / 接入版预览进行了 35 秒浏览器渲染进程 RSS 观察:约 193.3 MiB 至 177.2 MiB,未观察到持续增长。该结果仅覆盖本地短时对照预览;本轮没有重新引入 View Model 绑定,也未更新 Rive 版本。