@@ -8,10 +8,9 @@ export function WebGLShader() {
88 const lastFrameTimeRef = useRef < number > ( 0 ) ;
99 const [ isVisible , setIsVisible ] = useState ( true ) ;
1010 const [ prefersReducedMotion , setPrefersReducedMotion ] = useState ( false ) ;
11-
12- // Frame rate limiting - lower for Safari/Firefox
13- const targetFPS = 60 ;
14- const frameInterval = 1000 / targetFPS ;
11+ const [ forceStaticFallback , setForceStaticFallback ] = useState ( false ) ;
12+ const fpsRef = useRef < number > ( 60 ) ;
13+ const frameDropsRef = useRef < number > ( 0 ) ;
1514
1615 useEffect ( ( ) => {
1716 // Check for reduced motion preference
@@ -43,8 +42,27 @@ export function WebGLShader() {
4342 } , [ ] ) ;
4443
4544 useEffect ( ( ) => {
45+ if ( forceStaticFallback ) return ;
4646 if ( ! canvasRef . current ) return ;
4747
48+ // --- Hardware Feature Detection ---
49+ let dprMultiplier = 1.5 ;
50+ if ( typeof navigator !== "undefined" ) {
51+ const concurrency = navigator . hardwareConcurrency || 4 ;
52+ // @ts -expect-error deviceMemory is non-standard but widely supported in Chromium
53+ const memory = navigator . deviceMemory || 4 ;
54+
55+ if ( concurrency <= 2 || memory <= 2 ) {
56+ // Extremely low end device, skip WebGL entirely to save battery and avoid panics
57+ setForceStaticFallback ( true ) ;
58+ return ;
59+ } else if ( concurrency <= 4 || memory <= 4 ) {
60+ // Lower end device, start with conservative defaults
61+ fpsRef . current = 30 ;
62+ dprMultiplier = 1.0 ;
63+ }
64+ }
65+
4866 const canvas = canvasRef . current ;
4967 const gl = canvas . getContext ( "webgl" , {
5068 alpha : true ,
@@ -61,7 +79,7 @@ export function WebGLShader() {
6179 return ;
6280 }
6381
64- const pixelRatio = Math . min ( window . devicePixelRatio , 1.5 ) ;
82+ const pixelRatio = Math . min ( window . devicePixelRatio , dprMultiplier ) ;
6583
6684 const vertexShaderSource = `
6785 attribute vec2 position;
@@ -185,9 +203,26 @@ export function WebGLShader() {
185203
186204 // Frame rate limiting
187205 const elapsed = currentTime - lastFrameTimeRef . current ;
188- if ( elapsed < frameInterval ) return ;
206+ const currentFrameInterval = 1000 / fpsRef . current ;
207+
208+ if ( elapsed < currentFrameInterval ) return ;
209+
210+ // Dynamic Framerate Scaling (monitor requested framerate vs actual)
211+ // Ignore huge jumps (like switching tabs) which might spike the elapsed time
212+ if ( elapsed > currentFrameInterval * 1.5 && elapsed < 1000 ) {
213+ frameDropsRef . current ++ ;
214+ // If we consistently drop frames (e.g. 30 frames dropped), lower the target FPS
215+ if ( frameDropsRef . current > 30 && fpsRef . current > 30 ) {
216+ fpsRef . current = 30 ;
217+ frameDropsRef . current = 0 ;
218+ console . warn ( "WebGL Shader: Performance degraded, dynamically scaling target FPS down to 30." ) ;
219+ }
220+ } else {
221+ // Recover frame drop count if it's hitting targets consistently
222+ frameDropsRef . current = Math . max ( 0 , frameDropsRef . current - 1 ) ;
223+ }
189224
190- lastFrameTimeRef . current = currentTime - ( elapsed % frameInterval ) ;
225+ lastFrameTimeRef . current = currentTime - ( elapsed % currentFrameInterval ) ;
191226
192227 // Slower time progression
193228 time += 0.01 ;
@@ -211,10 +246,10 @@ export function WebGLShader() {
211246 gl . deleteProgram ( program ) ;
212247 gl . deleteBuffer ( buffer ) ;
213248 } ;
214- } , [ isVisible , prefersReducedMotion , frameInterval ] ) ;
249+ } , [ isVisible , prefersReducedMotion , forceStaticFallback ] ) ;
215250
216- // If reduced motion is preferred, show a static gradient instead
217- if ( prefersReducedMotion ) {
251+ // If reduced motion is preferred or hardware is too weak , show a static gradient instead
252+ if ( prefersReducedMotion || forceStaticFallback ) {
218253 return (
219254 < div
220255 className = "fixed inset-0 z-0 pointer-events-none"
0 commit comments