From 479de76ebbad46eebb6bd68a584382dcc9257306 Mon Sep 17 00:00:00 2001 From: LyAhn Date: Sun, 21 Jun 2026 08:48:05 +0100 Subject: [PATCH] perf: stop full re-sort on media-updated batches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/store.ts | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/store.ts b/src/store.ts index 05cbe35..2daca96 100644 --- a/src/store.ts +++ b/src/store.ts @@ -682,11 +682,24 @@ function replaceImage(images: ImageRecord[], updatedImage: ImageRecord, sort: So function replaceExistingImages( currentImages: ImageRecord[], updatedImages: ImageRecord[], - sort: SortOrder, ): 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 nextImages = currentImages.map((image) => updatesByPath.get(image.path) ?? image); - return nextImages.sort((a, b) => compareImages(a, b, sort)); + let changed = false; + 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 { @@ -2295,7 +2308,7 @@ export const useGalleryStore = create((set, get) => ({ } return { - images: replaceExistingImages(state.images, visibleImages, state.sort), + images: replaceExistingImages(state.images, visibleImages), selectedImage, }; });