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)
This commit is contained in:
Fringg
2026-02-23 17:14:47 +03:00
parent 0b4e8253aa
commit 9a84e13e6c

View File

@@ -132,12 +132,13 @@ let _webglAvailable: boolean | null = null;
function isWebglAvailable(): boolean { function isWebglAvailable(): boolean {
if (_webglAvailable === null) { if (_webglAvailable === null) {
try { try {
const renderer = new Renderer({ const canvas = document.createElement('canvas');
alpha: true, const gl = canvas.getContext('webgl2') || canvas.getContext('webgl');
antialias: false, _webglAvailable = !!gl;
powerPreference: 'low-power', if (gl) {
}); const loseCtx = gl.getExtension('WEBGL_lose_context');
_webglAvailable = !!renderer.gl; if (loseCtx) loseCtx.loseContext();
}
} catch { } catch {
_webglAvailable = false; _webglAvailable = false;
} }
@@ -258,15 +259,16 @@ function AuroraImpl() {
resize(); resize();
let lastTime = 0; let lastTime = 0;
const targetFPS = 20; const targetFPS = 10;
const frameInterval = 1000 / targetFPS; const frameInterval = 1000 / targetFPS;
const speed = 0.3; const speed = 0.3;
function animate(currentTime: number) { function animate(currentTime: number) {
animationFrameRef.current = requestAnimationFrame(animate);
const delta = currentTime - lastTime; const delta = currentTime - lastTime;
if (delta < frameInterval) return; if (delta < frameInterval) {
animationFrameRef.current = requestAnimationFrame(animate);
return;
}
lastTime = currentTime - (delta % frameInterval); lastTime = currentTime - (delta % frameInterval);
@@ -274,15 +276,45 @@ function AuroraImpl() {
programRef.current.uniforms.uTime.value += speed * 0.01; programRef.current.uniforms.uTime.value += speed * 0.01;
rendererRef.current.render({ scene: mesh }); 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); animationFrameRef.current = requestAnimationFrame(animate);
return () => { return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
window.removeEventListener('resize', resize); window.removeEventListener('resize', resize);
cancelAnimationFrame(animationFrameRef.current); 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; rendererRef.current = null;
programRef.current = null; programRef.current = null;
}; };