Merge pull request #2610 from rommapp/scan-page-performance

Improve scan page performance on large lists
This commit is contained in:
Georges-Antoine Assi
2025-10-28 16:17:51 -04:00
committed by GitHub
4 changed files with 89 additions and 48 deletions

View File

@@ -169,3 +169,12 @@ const { t } = useI18n();
</v-list-item>
</v-expansion-panel-text>
</template>
<style scoped>
.v-chip {
contain: layout style paint;
}
.v-avatar {
contain: layout style paint;
}
</style>

View File

@@ -16,7 +16,10 @@ const fallbackCoverImage = computed(() =>
<template>
<v-avatar :width="size" rounded="0">
<v-img :src="props.rom.path_cover_small || fallbackCoverImage">
<v-img
:src="props.rom.path_cover_small || fallbackCoverImage"
:alt="props.rom.name || props.rom.fs_name"
>
<template #error>
<v-img :src="fallbackCoverImage" />
</template>

View File

@@ -1,7 +1,8 @@
<script setup lang="ts">
import { debounce } from "lodash";
import type { Emitter } from "mitt";
import { storeToRefs } from "pinia";
import { inject, onBeforeUnmount } from "vue";
import { inject, onBeforeUnmount, ref } from "vue";
import { useI18n } from "vue-i18n";
import type { ScanStats } from "@/__generated__";
import socket from "@/services/socket";
@@ -35,6 +36,54 @@ const romsStore = storeRoms();
// Connect to socket on load to catch running scans
if (!socket.connected) socket.connect();
// Batch ROM updates to prevent excessive re-renders
const romUpdateQueue = ref<SimpleRom[]>([]);
const processRomUpdates = debounce(() => {
if (romUpdateQueue.value.length === 0) return;
const updates = [...romUpdateQueue.value];
romUpdateQueue.value = [];
updates.forEach((rom) => {
// Remove the ROM from the recent list and add it back to the top
romsStore.removeFromRecent(rom);
romsStore.addToRecent(rom);
if (romsStore.currentPlatform?.id === rom.platform_id) {
const existingRom = romsStore.filteredRoms.find((r) => r.id === rom.id);
existingRom ? romsStore.update(rom) : romsStore.add([rom]);
}
let scannedPlatform = scanningPlatforms.value.find(
(p) => p.slug === rom.platform_slug,
);
// Add the platform if the socket dropped and it's missing
if (!scannedPlatform) {
scanningPlatforms.value.push({
display_name: rom.platform_display_name,
slug: rom.platform_slug,
id: rom.platform_id,
fs_slug: rom.platform_fs_slug,
is_identified: true,
roms: [],
});
scannedPlatform = scanningPlatforms.value.at(-1)!;
}
// Check if ROM already exists in the store
const existingRomInPlatform = scannedPlatform?.roms.find(
(r) => r.id === rom.id,
);
if (existingRomInPlatform) {
scannedPlatform.roms = scannedPlatform.roms.map((r) =>
r.id === rom.id ? rom : r,
);
} else {
scannedPlatform?.roms.push(rom);
}
});
}, 100);
socket.on(
"scan:scanning_platform",
({
@@ -68,45 +117,9 @@ socket.on(
socket.on("scan:scanning_rom", (rom: SimpleRom) => {
scanningStore.setScanning(true);
// Remove the ROM from the recent list and add it back to the top
romsStore.removeFromRecent(rom);
romsStore.addToRecent(rom);
if (romsStore.currentPlatform?.id === rom.platform_id) {
const existingRom = romsStore.filteredRoms.find((r) => r.id === rom.id);
if (existingRom) {
romsStore.update(rom);
} else {
romsStore.add([rom]);
}
}
let scannedPlatform = scanningPlatforms.value.find(
(p) => p.slug === rom.platform_slug,
);
// Add the platform if the socket dropped and it's missing
if (!scannedPlatform) {
scanningPlatforms.value.push({
display_name: rom.platform_display_name,
slug: rom.platform_slug,
id: rom.platform_id,
fs_slug: rom.platform_fs_slug,
is_identified: true,
roms: [],
});
scannedPlatform = scanningPlatforms.value[0];
}
// Check if ROM already exists in the store
const existingRom = scannedPlatform?.roms.find((r) => r.id === rom.id);
if (existingRom) {
scannedPlatform.roms = scannedPlatform.roms.map((r) =>
r.id === rom.id ? rom : r,
);
} else {
scannedPlatform?.roms.push(rom);
}
// Queue ROM for batch processing instead of immediate update
romUpdateQueue.value.push(rom);
processRomUpdates();
});
socket.on("scan:done", () => {
@@ -142,6 +155,7 @@ onBeforeUnmount(() => {
socket.off("scan:scanning_rom");
socket.off("scan:done");
socket.off("scan:done_ko");
processRomUpdates.cancel();
});
</script>
<template>

View File

@@ -1,3 +1,4 @@
import { throttle } from "lodash";
import { onUnmounted, watchEffect, nextTick, type ShallowRef } from "vue";
export const useAutoScroll = (
@@ -9,7 +10,7 @@ export const useAutoScroll = (
let isUserScrolled = false;
let observer: MutationObserver | null = null;
const scrollToBottom = () => {
const scrollToBottom = throttle(() => {
const containerEl = scrollContainer.value?.$el;
if (!containerEl) return;
@@ -17,7 +18,7 @@ export const useAutoScroll = (
top: containerEl.scrollHeight,
behavior: config.smooth ? "smooth" : "instant",
});
};
}, 50);
const init = () => {
const containerEl = scrollContainer.value?.$el;
@@ -31,18 +32,32 @@ export const useAutoScroll = (
containerEl.scrollHeight;
});
// Auto-scroll on content changes
observer = new MutationObserver((e: MutationRecord[]) => {
// Auto-scroll on content changes with throttled observer
observer = new MutationObserver((mutations: MutationRecord[]) => {
if (!config.always && isUserScrolled) return;
if (e[e.length - 1].addedNodes.length === 0) return;
scrollToBottom();
// Only process if there are actual node additions
const hasNewNodes = mutations.some(
(mutation) =>
mutation.type === "childList" && mutation.addedNodes.length > 0,
);
if (hasNewNodes) {
scrollToBottom();
}
});
observer.observe(observedEl, { childList: true, subtree: config.deep });
observer.observe(observedEl, {
childList: true,
subtree: config.deep,
attributes: false,
characterData: false,
});
scrollToBottom();
};
const cleanup = () => {
scrollToBottom.cancel();
observer?.disconnect();
observer = null;
};