首页的 3D 机械键盘有近百个键帽,每个键帽的面上贴着一张技能图标纹理。某次调整资源路径后打开页面——键盘没了。没有红色报错,没有白屏,控制台干净得像什么都没发生,只有本该有键盘的地方空着。
3D 世界里的失败是沉默的
DOM 时代我们被惯坏了:图片挂了有 onerror,组件炸了有错误边界,资源 404 网络面板一眼可见。而 WebGL 的资源管线是另一个脾气:
- 纹理 404 → three.js 打个 console warning,继续渲染
键帽面上是黑的,场景照常跑
- shader 编译错 → 顶点全被丢弃,画布全黑
- 上下文丢失 → 整个 canvas 冻结在最后一帧每一步都"成功执行"了,只是结果不对。这类 bug 的可怕之处在于:它不触发你的任何监控,除非你盯着画面看。
对策:把沉默变成状态机
吃了一次亏之后,场景被包进了一个极简的健康状态机:
export type SceneStatus = "idle" | "loading" | "ready" | "failed";
// useSyncExternalStore 订阅,React 组件随状态切换降级场景加载失败 → failed → 技能网格自动顶上来(就是你在禁用 WebGL 的设备上看到的那个朴素网格)。技能信息从一开始就在 DOM 里以 sr-only 的形式存在——3D 只是它的"演出形态",不是它的本体。
顺手的副产品
状态机建立后,一系列原本要单独处理的情况自动获得了正确行为:prefers-reduced-motion 用户、WebGL 不可用的老设备、低端机上主动关闭 3D 的性能档位——全部走同一条降级路径,技能区永远可读。
3D 场景的正确心智模型不是"页面的一个部分",而是"一段可能失败的演出"。演出可以取消,但节目单必须一直印在票面上。