From 43d994f843a92331984313bf682c3e8dd467847e Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Sat, 13 Dec 2025 17:51:58 +0100 Subject: [PATCH] Keep unchanged metadata stable for memoization Everytime the metadata got updated, unchanged values could cause rerenders because their reference changed. This happened e.g., for the "customThemes" which caused the MUI theme to get recreated resulting in all components to rerender due to the theme context change. --- .../metadata/services/MetadataReader.ts | 67 ++++++++++++++++--- .../metadata/services/MetadataValueCache.ts | 40 +++++++++++ 2 files changed, 96 insertions(+), 11 deletions(-) create mode 100644 src/features/metadata/services/MetadataValueCache.ts diff --git a/src/features/metadata/services/MetadataReader.ts b/src/features/metadata/services/MetadataReader.ts index 9b3154d7..5b401531 100644 --- a/src/features/metadata/services/MetadataReader.ts +++ b/src/features/metadata/services/MetadataReader.ts @@ -23,6 +23,48 @@ import { applyMetadataMigrations } from '@/features/metadata/services/MetadataMi import { SourceIdInfo } from '@/features/source/Source.types.ts'; import { ChapterIdInfo } from '@/features/chapter/Chapter.types.ts'; import { APP_METADATA } from '@/features/metadata/Metadata.constants.ts'; +import { MetadataValueCache } from '@/features/metadata/services/MetadataValueCache.ts'; + +const getHolderId = ( + type: MetadataHolderType, + metadataHolder: + | MetadataHolder + | (MangaIdInfo & MetadataHolder) + | (ChapterIdInfo & MetadataHolder) + | (CategoryIdInfo & MetadataHolder) + | (SourceIdInfo & MetadataHolder), +): string | number | undefined => { + switch (type) { + case 'global': + return undefined; + case 'manga': + return (metadataHolder as MangaIdInfo).id; + case 'chapter': + return (metadataHolder as ChapterIdInfo).id; + case 'category': + return (metadataHolder as CategoryIdInfo).id; + case 'source': + return (metadataHolder as SourceIdInfo).id; + default: + return undefined; + } +}; + +const getRawMetadataValueFrom = ( + metadata: Metadata | undefined, + key: string, + prefixes?: string[], +): string | undefined => { + if ( + metadata === undefined || + !doesMetadataKeyExistIn(metadata, key, prefixes) || + metadata[getMetadataKey(key, prefixes)] === undefined + ) { + return undefined; + } + + return metadata[getMetadataKey(key, prefixes)]; +}; const getMetadataValueFrom = ( metadata: Metadata | undefined, @@ -30,15 +72,13 @@ const getMetadataValueFrom = { - if ( - metadata === undefined || - !doesMetadataKeyExistIn(metadata, key, prefixes) || - metadata[getMetadataKey(key, prefixes)] === undefined - ) { + const rawValue = getRawMetadataValueFrom(metadata, key, prefixes); + + if (rawValue === undefined) { return defaultValue; } - const convertedValue = convertValueFromMetadata(key, metadata[getMetadataKey(key, prefixes)], defaultValue); + const convertedValue = convertValueFromMetadata(key, rawValue, defaultValue); return APP_METADATA[key].toConstrainedValue?.(convertedValue) ?? convertedValue; }; @@ -92,13 +132,18 @@ export function getMetadataFrom { - appMetadata[key as AppMetadataKeys] = getMetadataValueFrom( - migratedMetadata, - key as AppMetadataKeys, - defaultValue, - prefixes, + const rawValue = getRawMetadataValueFrom(migratedMetadata, key as AppMetadataKeys, prefixes); + const newValue = getMetadataValueFrom(migratedMetadata, key as AppMetadataKeys, defaultValue, prefixes); + + appMetadata[key as AppMetadataKeys] = MetadataValueCache.getStableValue( + type, + holderId, + getMetadataKey(key, prefixes), + rawValue, + newValue, ); }); diff --git a/src/features/metadata/services/MetadataValueCache.ts b/src/features/metadata/services/MetadataValueCache.ts new file mode 100644 index 00000000..e5015d25 --- /dev/null +++ b/src/features/metadata/services/MetadataValueCache.ts @@ -0,0 +1,40 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import { MetadataHolderType } from '@/features/metadata/Metadata.types.ts'; + +export class MetadataValueCache { + private static convertedValueByKey = new Map(); + + private static rawValueByKey = new Map(); + + private static getCacheKey(type: MetadataHolderType, holderId: string | number | undefined, key: string): string { + return `${type}::${holderId ?? ''}::${key}`; + } + + static getStableValue( + type: MetadataHolderType, + holderId: string | number | undefined, + key: string, + rawValue: string | undefined, + newValue: T, + ): T { + const cacheKey = this.getCacheKey(type, holderId, key); + const cachedRawValue = this.rawValueByKey.get(cacheKey); + const cachedConvertedValue = this.convertedValueByKey.get(cacheKey); + + if (cachedRawValue !== undefined && rawValue === cachedRawValue) { + return cachedConvertedValue as T; + } + + this.rawValueByKey.set(cacheKey, rawValue); + this.convertedValueByKey.set(cacheKey, newValue); + + return newValue; + } +}