11/**
2- * 性能监控 — FPS追踪与动态降级
2+ * 性能监控 — FPS追踪与动态降级(基于 drei 成熟方案)
33 *
4- * 降级策略(防止“动画闪烁/长期隐藏”的三项关键设计):
5- * 1. 滞回:低于 25 记为低、高于 50 记为高,25~50 为缓冲区不触发变更,
6- * 避免 FPS 在单一阈值附近振荡导致 tier 来回抖动;
7- * 2. 持续判定:同方向连续 2 个窗口(4s)才逐级调整 tier,
8- * 防止瞬时卡顿(着色器编译、GC、场景切换)造成立即降级;
9- * 3. 后台重置:从后台返回时重置测量基线,避免浏览器后台 rAF 节流
10- * 被误判为性能恶化、回到前台后立即降级。
4+ * 重构说明(docs/standards/refactoring.md,外部行为不变):
5+ * - FPS 测量与防抖委托给 drei <PerformanceMonitor>(工业级实现):
6+ * 内置滑动均值(iterations×ms 采样)、bounds 滞回、threshold 比例持续判定
7+ * 与 flip-flop 保护,替代原自研单窗口测量 + 手写滞回/持续逻辑;
8+ * - tier 迁移策略抽取至纯函数模块 tierPolicy(已单测);
9+ * - 本组件仅做装配:把 drei 回调映射为 tier 逐级迁移,并在窗口隐藏时
10+ * 卸载监控以重置测量基线(后台 rAF 节流期 FPS 不可信,卸载重挂即自然
11+ * 清零,避免回到前台被误判降级)。
1112 *
1213 * @ai -context: 3D 场景核心(R3F):PerformanceMonitor。
1314 */
14- import { useFrame } from '@ react-three/fiber ' ;
15- import { useEffect , useRef } from 'react' ;
15+ import { useEffect , useState } from 'react' ;
16+ import { PerformanceMonitor as DreiPerformanceMonitor } from '@ react-three/drei ' ;
1617import { create } from 'zustand' ;
18+ import { stepTier , type PerformanceTier } from './tierPolicy' ;
1719
18- type PerformanceTier = 'high' | 'medium' | 'low' ;
20+ /** FPS 下界:持续低于此值触发降级(滞回缓冲区下限) */
21+ export const FPS_LOWER_BOUND = 25 ;
22+ /** FPS 上界:持续高于此值触发升级(滞回缓冲区上限) */
23+ export const FPS_UPPER_BOUND = 50 ;
24+ /** 升降震荡(flip-flop)允许次数上限,超过则回落稳定档 */
25+ const MAX_FLIPFLOPS = 4 ;
1926
2027interface PerformanceState {
2128 tier : PerformanceTier ;
@@ -31,73 +38,39 @@ export const usePerformanceStore = create<PerformanceState>((set) => ({
3138 setFps : ( fps ) => set ( { fps } ) ,
3239} ) ) ;
3340
34- /** FPS 阈值:低于 LOWER 计低、高于 UPPER 计高,中间为缓冲区(滞回) */
35- const LOWER_BOUND = 25 ;
36- const UPPER_BOUND = 50 ;
37- /** 触发 tier 变更所需的同方向连续窗口数(持续判定,每窗口 2s) */
38- const SUSTAIN_WINDOWS = 2 ;
39- /** FPS 测量窗口长度(ms) */
40- const WINDOW_MS = 2000 ;
41-
4241export function PerformanceMonitor ( ) {
43- const frameCount = useRef ( 0 ) ;
44- const lastTime = useRef ( performance . now ( ) ) ;
45- const lowStreak = useRef ( 0 ) ;
46- const highStreak = useRef ( 0 ) ;
47-
48- // 从后台返回后重置测量基线:后台期间浏览器节流 rAF,
49- // 若不重置,返回后的第一个窗口会测出极低 FPS 被误判为降级
42+ // 窗口隐藏时卸载监控:后台期间浏览器节流 rAF,FPS 读数不可信;
43+ // 返回前台重挂即自然重置采样基线,避免误判降级。
44+ // 附带收益:同时重置 drei 内部 flipped/fallback 计数,防止 fallback 永久锁定。
45+ const [ visible , setVisible ] = useState ( ( ) => document . visibilityState === 'visible' ) ;
5046 useEffect ( ( ) => {
51- const reset = ( ) => {
52- frameCount . current = 0 ;
53- lastTime . current = performance . now ( ) ;
54- lowStreak . current = 0 ;
55- highStreak . current = 0 ;
56- } ;
57- const onVisibility = ( ) => {
58- if ( document . visibilityState === 'visible' ) reset ( ) ;
59- } ;
47+ const onVisibility = ( ) => setVisible ( document . visibilityState === 'visible' ) ;
6048 document . addEventListener ( 'visibilitychange' , onVisibility ) ;
6149 return ( ) => document . removeEventListener ( 'visibilitychange' , onVisibility ) ;
6250 } , [ ] ) ;
6351
64- useFrame ( ( ) => {
65- frameCount . current ++ ;
66- const now = performance . now ( ) ;
67- const elapsed = now - lastTime . current ;
68- if ( elapsed < WINDOW_MS ) return ;
69-
70- const fps = Math . round ( frameCount . current * ( 1000 / elapsed ) ) ;
71- frameCount . current = 0 ;
72- lastTime . current = now ;
73-
74- // 在帧回调内取最新 tier,避免闭包捕获旧值
75- const { tier, setTier, setFps } = usePerformanceStore . getState ( ) ;
76- setFps ( fps ) ;
77-
78- if ( fps < LOWER_BOUND ) {
79- lowStreak . current ++ ;
80- highStreak . current = 0 ;
81- } else if ( fps > UPPER_BOUND ) {
82- highStreak . current ++ ;
83- lowStreak . current = 0 ;
84- } else {
85- // 缓冲区:双向计数清零,tier 保持不变
86- lowStreak . current = 0 ;
87- highStreak . current = 0 ;
88- }
89-
90- // 持续同方向才逐级调整一级,避免悬崖式变更(high→low 会直接隐藏后处理)
91- if ( lowStreak . current >= SUSTAIN_WINDOWS ) {
92- lowStreak . current = 0 ;
93- if ( tier === 'high' ) setTier ( 'medium' ) ;
94- else if ( tier === 'medium' ) setTier ( 'low' ) ;
95- } else if ( highStreak . current >= SUSTAIN_WINDOWS ) {
96- highStreak . current = 0 ;
97- if ( tier === 'low' ) setTier ( 'medium' ) ;
98- else if ( tier === 'medium' ) setTier ( 'high' ) ;
99- }
100- } ) ;
52+ if ( ! visible ) return null ;
10153
102- return null ;
54+ return (
55+ < DreiPerformanceMonitor
56+ bounds = { ( ) => [ FPS_LOWER_BOUND , FPS_UPPER_BOUND ] }
57+ flipflops = { MAX_FLIPFLOPS }
58+ onIncline = { ( api ) => {
59+ // 持续高 FPS(75%+ 样本 ≥ 上界)→ 逐级升档
60+ const { tier, setTier, setFps } = usePerformanceStore . getState ( ) ;
61+ setFps ( Math . round ( api . fps ) ) ;
62+ setTier ( stepTier ( tier , 'up' ) ) ;
63+ } }
64+ onDecline = { ( api ) => {
65+ // 持续低 FPS(75%+ 样本 < 下界)→ 逐级降档
66+ const { tier, setTier, setFps } = usePerformanceStore . getState ( ) ;
67+ setFps ( Math . round ( api . fps ) ) ;
68+ setTier ( stepTier ( tier , 'down' ) ) ;
69+ } }
70+ onFallback = { ( ) => {
71+ // 震荡超限 → 稳定在中档:保留后处理特效、适度降低 DPR
72+ usePerformanceStore . getState ( ) . setTier ( 'medium' ) ;
73+ } }
74+ />
75+ ) ;
10376}
0 commit comments