性能分层
Realm 的 hooks/useAdaptivePerformance.ts 在客户端维持一个四档自适应 PerformanceTier,把不同效果绑到「能力位」上。组件读能力位而不是直接判断 navigator.userAgent 或 window.innerWidth。
四档与能力位
logo 在所有档都可见,只是 balanced 上去掉 pointer listener、animation frame loop、彩色 drop shadow。能力位由 buildPerformanceState() 一次性映射,组件读 useAdaptivePerformance() 的返回值即可。
启发式上限
resolveHeuristic() 在 hydration 后从环境信号推出一个上限档(maxTier),运行时档不会超过这个上限:
SSR 阶段用不到 client-only 信号,临时给 full。
运行时采样
动画序列结束(animationsComplete)后启动一个 requestAnimationFrame 采样循环,每个采样窗口最长 120 帧 或 2500ms,以先到为准:
- 帧间隔 > 32ms 视为「慢帧」
- 平均 FPS < 45 或 慢帧比 ≥ 25% → poor window
- 平均 FPS ≥ 55 且 慢帧比 ≤ 10% → healthy window
档位迁移规则:
- 连续 2 个 poor window → 降一档(5s 冷却)
- 连续 3 个 healthy window → 升一档(10s 冷却;且不能超过启发式上限)
- 其他 → 不变
lastTierChangeRef 维护冷却时间。reason 在 runtime 切换时变为 'runtime-fps',回到 maxTier 时回到 null。
档位变化写入 <html data-performance-tier="...">,CSS 可以基于此属性做条件样式(比如关掉某些动画)。
添加新效果时的约束
- 不要在组件里读
navigator.userAgent/navigator.deviceMemory/window.matchMedia('(prefers-reduced-motion)')——一律读useAdaptivePerformance()返回的能力位。 - 任何「可选」效果必须能容忍被卸载,因为 tier 变化会触发
useEffectcleanup。 - 模块加载失败要 swallow(已经有重试在更外层做),不要污染上层错误。