Skip to content

Commit 9bc528f

Browse files
committed
feat: add WebGLShader component for dynamic background with performance optimizations, hardware detection, and static fallback.
1 parent a2229bd commit 9bc528f

File tree

1 file changed

+45
-10
lines changed

1 file changed

+45
-10
lines changed

components/ui/web-gl-shader.tsx

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)