From 9a84e13e6cd4dcc3a6d5e7f95fddb4c9c1ec076e Mon Sep 17 00:00:00 2001 From: Fringg Date: Mon, 23 Feb 2026 17:14:47 +0300 Subject: [PATCH] perf: fix critical WebGL GPU resource leaks in Aurora - Fix isWebglAvailable() leaking a full WebGL context on every check by replacing OGL Renderer with raw canvas.getContext() + loseContext() - Add visibilitychange listener to fully stop rAF loop when tab hidden and restart on visibility restore (was running 24/7 in background) - Properly destroy WebGL context on unmount via WEBGL_lose_context extension and gl.deleteProgram() (was only nulling JS refs) - Reduce target FPS from 20 to 10 (slow gradient, visually identical) --- src/components/layout/AppShell/Aurora.tsx | 56 ++++++++++++++++++----- 1 file changed, 44 insertions(+), 12 deletions(-) diff --git a/src/components/layout/AppShell/Aurora.tsx b/src/components/layout/AppShell/Aurora.tsx index 278f70f..534dc00 100644 --- a/src/components/layout/AppShell/Aurora.tsx +++ b/src/components/layout/AppShell/Aurora.tsx @@ -132,12 +132,13 @@ let _webglAvailable: boolean | null = null; function isWebglAvailable(): boolean { if (_webglAvailable === null) { try { - const renderer = new Renderer({ - alpha: true, - antialias: false, - powerPreference: 'low-power', - }); - _webglAvailable = !!renderer.gl; + const canvas = document.createElement('canvas'); + const gl = canvas.getContext('webgl2') || canvas.getContext('webgl'); + _webglAvailable = !!gl; + if (gl) { + const loseCtx = gl.getExtension('WEBGL_lose_context'); + if (loseCtx) loseCtx.loseContext(); + } } catch { _webglAvailable = false; } @@ -258,15 +259,16 @@ function AuroraImpl() { resize(); let lastTime = 0; - const targetFPS = 20; + const targetFPS = 10; const frameInterval = 1000 / targetFPS; const speed = 0.3; function animate(currentTime: number) { - animationFrameRef.current = requestAnimationFrame(animate); - const delta = currentTime - lastTime; - if (delta < frameInterval) return; + if (delta < frameInterval) { + animationFrameRef.current = requestAnimationFrame(animate); + return; + } lastTime = currentTime - (delta % frameInterval); @@ -274,15 +276,45 @@ function AuroraImpl() { programRef.current.uniforms.uTime.value += speed * 0.01; rendererRef.current.render({ scene: mesh }); } + + animationFrameRef.current = requestAnimationFrame(animate); } + + function handleVisibilityChange() { + if (document.hidden) { + cancelAnimationFrame(animationFrameRef.current); + animationFrameRef.current = 0; + } else { + lastTime = 0; + animationFrameRef.current = requestAnimationFrame(animate); + } + } + document.addEventListener('visibilitychange', handleVisibilityChange); + animationFrameRef.current = requestAnimationFrame(animate); return () => { + document.removeEventListener('visibilitychange', handleVisibilityChange); window.removeEventListener('resize', resize); cancelAnimationFrame(animationFrameRef.current); - if (rendererRef.current && container.contains(rendererRef.current.gl.canvas)) { - container.removeChild(rendererRef.current.gl.canvas); + + if (rendererRef.current) { + const glCtx = rendererRef.current.gl; + + // Delete GPU resources + if (programRef.current) { + glCtx.deleteProgram(programRef.current.program); + } + + // Force-release the WebGL context + const loseCtx = glCtx.getExtension('WEBGL_lose_context'); + if (loseCtx) loseCtx.loseContext(); + + if (container.contains(glCtx.canvas)) { + container.removeChild(glCtx.canvas); + } } + rendererRef.current = null; programRef.current = null; };