fix: prevent memory leaks on consecutive fight replays
- FightViewer: store canvas event handlers and remove them before replacing canvas elements, preventing detached DOM/closure leaks - FightViewer: clean up canvas listeners on unmount - tts.ts: clear _staticLoading dedup map after precache completes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
aa263ec8ae
commit
854b1cd1df
@@ -30,6 +30,8 @@ let cleanupTimerHandle: ReturnType<typeof setTimeout> | null = null
|
||||
let destroyed = false
|
||||
let initializingScene = false
|
||||
const contextLost = ref(false)
|
||||
let canvasContextLostHandler: ((e: Event) => void) | null = null
|
||||
let canvasContextRestoredHandler: (() => void) | null = null
|
||||
|
||||
const isReplaying = ref(false)
|
||||
const ttsProgress = ref(-1)
|
||||
@@ -171,6 +173,14 @@ onUnmounted(() => {
|
||||
stopAllAudio()
|
||||
sceneReady.value = false
|
||||
if (scene) { scene.destroy(); scene = null }
|
||||
// Remove canvas event listeners to prevent detached DOM leaks
|
||||
const canvas = canvasRef.value
|
||||
if (canvas && canvasContextLostHandler) {
|
||||
canvas.removeEventListener('webglcontextlost', canvasContextLostHandler)
|
||||
canvas.removeEventListener('webglcontextrestored', canvasContextRestoredHandler!)
|
||||
}
|
||||
canvasContextLostHandler = null
|
||||
canvasContextRestoredHandler = null
|
||||
if (cleanupTimerHandle) { clearTimeout(cleanupTimerHandle); cleanupTimerHandle = null }
|
||||
if (ttsProgressTimer) { clearInterval(ttsProgressTimer); ttsProgressTimer = null }
|
||||
if (typeof speechSynthesis !== 'undefined') speechSynthesis.cancel()
|
||||
@@ -194,6 +204,11 @@ async function initScene() {
|
||||
|
||||
// Replace canvas element so Kaplay gets a fresh context
|
||||
const oldCanvas = canvasRef.value
|
||||
// Remove old canvas event listeners to prevent memory leaks
|
||||
if (oldCanvas && canvasContextLostHandler) {
|
||||
oldCanvas.removeEventListener('webglcontextlost', canvasContextLostHandler)
|
||||
oldCanvas.removeEventListener('webglcontextrestored', canvasContextRestoredHandler!)
|
||||
}
|
||||
const newCanvas = document.createElement('canvas')
|
||||
newCanvas.className = 'w-full h-full block'
|
||||
newCanvas.width = container.clientWidth || 800
|
||||
@@ -207,17 +222,19 @@ async function initScene() {
|
||||
contextLost.value = false
|
||||
|
||||
// Handle WebGL/Canvas context loss (GPU pressure, tab backgrounding, etc.)
|
||||
newCanvas.addEventListener('webglcontextlost', (e) => {
|
||||
canvasContextLostHandler = (e: Event) => {
|
||||
e.preventDefault() // Allow context restoration
|
||||
contextLost.value = true
|
||||
sceneReady.value = false
|
||||
})
|
||||
newCanvas.addEventListener('webglcontextrestored', () => {
|
||||
}
|
||||
canvasContextRestoredHandler = () => {
|
||||
// Don't auto-reinit during replay — just mark context as restored
|
||||
// The contextLost overlay will let users tap to reload if needed
|
||||
contextLost.value = false
|
||||
if (!isReplaying.value) initScene()
|
||||
})
|
||||
}
|
||||
newCanvas.addEventListener('webglcontextlost', canvasContextLostHandler)
|
||||
newCanvas.addEventListener('webglcontextrestored', canvasContextRestoredHandler)
|
||||
|
||||
// Wrap scene creation in try/catch + timeout so a mobile sprite loading failure
|
||||
// or hang never blocks the overlay/voice/round flow
|
||||
|
||||
@@ -358,6 +358,8 @@ async function _precacheCommon() {
|
||||
}
|
||||
_precacheDone = true
|
||||
_precacheRunning = false
|
||||
// Release dedup promises — they hold closures referencing fetch/decode state
|
||||
_staticLoading.clear()
|
||||
}
|
||||
|
||||
function _cacheKey(text: string, profile: string): string {
|
||||
|
||||
Reference in New Issue
Block a user