perf: stop full re-sort on media-updated batches

replaceExistingImages re-sorted the entire loaded image array on every
media-updated event. Harmless for the ~200-item gallery window, but in Timeline
(which loads the whole library) it was an O(n log n) pass many times per second
during background indexing — severe lag, occasional crashes. Thumbnail/metadata
fills don't change list position (Timeline re-buckets by taken_at separately), so
replace records in place and skip the sort; return the same array reference when
nothing matched to avoid a wasted re-render.
This commit is contained in:
2026-06-21 08:48:05 +01:00
parent 21f6c30d25
commit 479de76ebb
+17 -4
View File
@@ -682,11 +682,24 @@ function replaceImage(images: ImageRecord[], updatedImage: ImageRecord, sort: So
function replaceExistingImages( function replaceExistingImages(
currentImages: ImageRecord[], currentImages: ImageRecord[],
updatedImages: ImageRecord[], updatedImages: ImageRecord[],
sort: SortOrder,
): ImageRecord[] { ): ImageRecord[] {
// Replace matched records in place WITHOUT re-sorting. `media-updated` carries
// thumbnail/metadata fills that don't move an item in the list (Timeline
// re-buckets by taken_at separately), and it fires constantly while the
// background workers run. Re-sorting here meant an O(n log n) pass on every
// batch — fine for the ~200-item gallery window, but a UI-freezing churn in
// Timeline view where `images` can hold the entire library (TIMELINE_PAGE_SIZE).
// Returning the same array reference when nothing matched also avoids a wasted
// re-render. Relative order for just-updated items is corrected on next load.
const updatesByPath = new Map(updatedImages.map((image) => [image.path, image])); const updatesByPath = new Map(updatedImages.map((image) => [image.path, image]));
const nextImages = currentImages.map((image) => updatesByPath.get(image.path) ?? image); let changed = false;
return nextImages.sort((a, b) => compareImages(a, b, sort)); const nextImages = currentImages.map((image) => {
const update = updatesByPath.get(image.path);
if (!update) return image;
changed = true;
return update;
});
return changed ? nextImages : currentImages;
} }
export function tileSizeForZoom(zoomPreset: ZoomPreset): number { export function tileSizeForZoom(zoomPreset: ZoomPreset): number {
@@ -2295,7 +2308,7 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
} }
return { return {
images: replaceExistingImages(state.images, visibleImages, state.sort), images: replaceExistingImages(state.images, visibleImages),
selectedImage, selectedImage,
}; };
}); });