perf(frontend): fix resource leaks and disable deep reactivity on media index

- useMediaWebSocket: shallowRef mediaIndex/tasks/loading/error/connected
  to eliminate Proxy overhead on the entire library data structure.
- useMediaWebSocket: replace per-task setTimeout leak with single 3s
  sweep interval for completed task cleanup.
- App.vue: remove {deep:true} watcher on mediaIndex; shallow ref change
  is sufficient to trigger search worker sync.
- useGamepadNavigation: rAF only when gamepads are connected; idle
  fallback to 500ms setTimeout to stop permanent 60fps CPU drain.
- useKeyboardNavigation: deduplicate synced scroll rAF requests to
  prevent overlapping animation frames.
- MediaDetail/SeriesFullView/CollageHero: pause, clear src, and load()
  video elements on unmount and before ref replacement to release
  decoder/memory resources.
- MediaDetail/SeriesFullView: clear all volume fade intervals on
  unmount to stop interval timer leaks.
This commit is contained in:
2026-05-27 20:28:51 +00:00
parent 79d3f40162
commit 96c2d52ee0
7 changed files with 149 additions and 28 deletions
+11
View File
@@ -181,9 +181,20 @@ onMounted(() => {
})
})
function cleanupHeroVideos() {
// Find all video elements inside the hero and explicitly release them
const videos = document.querySelectorAll<HTMLVideoElement>(".collage-hero video")
videos.forEach((video) => {
video.pause()
video.src = ""
video.load()
})
}
onUnmounted(() => {
window.removeEventListener("resize", updateVisibility)
document.removeEventListener("focusin", handleDocumentFocusIn)
cleanupHeroVideos()
})
function clearHeroFocus() {
+34 -1
View File
@@ -300,7 +300,18 @@ function registerMovieOutOfBoundsShortcut() {
})
}
function cleanupVideo(video: HTMLVideoElement | null | undefined) {
if (!video) return
video.pause()
video.src = ""
video.load()
}
function setVideoRef(el: HTMLVideoElement | null, index: number) {
const old = videoRefs.value[index]
if (old && old !== el) {
cleanupVideo(old)
}
videoRefs.value[index] = el
}
@@ -450,7 +461,19 @@ const collageSlots = computed(() => {
watch(
collageSlots,
async (slots) => {
async (slots, oldSlots) => {
// Pause and unload videos that are no longer referenced before reassigning refs
if (oldSlots) {
for (let i = 0; i < oldSlots.length; i++) {
const oldPaths = oldSlots[i]?.sourcePaths ?? []
const newPaths = slots[i]?.sourcePaths ?? []
const changed =
oldPaths.length !== newPaths.length || oldPaths.some((p, idx) => p !== newPaths[idx])
if (changed) {
cleanupVideo(videoRefs.value[i])
}
}
}
videoRefs.value = Array.from(
{ length: COLLAGE_SLOT_COUNT },
(_, index) => videoRefs.value[index] ?? null,
@@ -722,6 +745,16 @@ onUnmounted(() => {
disposeOutOfBoundsHandler?.()
disposeOutOfBoundsHandler = null
lastReleaseShortcutRow = null
// Clear all volume fade intervals
for (const interval of volumeFadeIntervals.values()) {
clearInterval(interval)
}
volumeFadeIntervals.clear()
// Pause and unload all video elements
for (const video of videoRefs.value) {
cleanupVideo(video)
}
videoRefs.value = []
})
</script>
@@ -468,6 +468,13 @@ const videoRefs = ref<Map<string, HTMLVideoElement>>(new Map())
let videoIndex = 0
const safariAutoplay = isSafariBrowser()
function cleanupVideo(video: HTMLVideoElement | null | undefined) {
if (!video) return
video.pause()
video.src = ""
video.load()
}
// Set video ref with staggered playback
function setVideoRef(el: HTMLVideoElement | null, key: string) {
if (el) {
@@ -484,6 +491,10 @@ function setVideoRef(el: HTMLVideoElement | null, key: string) {
safariAutoplay ? 0 : index * 200,
)
} else {
const old = videoRefs.value.get(key)
if (old) {
cleanupVideo(old)
}
videoRefs.value.delete(key)
}
}
@@ -649,6 +660,16 @@ onMounted(() => {
onUnmounted(() => {
window.removeEventListener("mediahive:gamepad-action", handleGamepadAction as EventListener)
// Clear all volume fade intervals
for (const interval of volumeFadeIntervals.values()) {
clearInterval(interval)
}
volumeFadeIntervals.clear()
// Pause and unload all video elements
for (const video of videoRefs.value.values()) {
cleanupVideo(video)
}
videoRefs.value.clear()
})
</script>