diff --git a/lib/thumbhash.ts b/lib/thumbhash.ts index f0c1f09..03c17cb 100644 --- a/lib/thumbhash.ts +++ b/lib/thumbhash.ts @@ -6,13 +6,32 @@ import { thumbHashToRGBA, thumbHashToDataURL } from 'thumbhash'; +// Bounded caches for expensive ThumbHash decoding operations +const CACHE_LIMIT = 1000; +const blurDataUrlCache = new Map(); +const dominantHexCache = new Map(); + +function setCache(map: Map, key: K, value: V) { + if (map.size >= CACHE_LIMIT) { + const firstKey = map.keys().next().value; + // Evict the first key using length check to avoid falsy string issue + if (firstKey !== undefined) map.delete(firstKey); + } + map.set(key, value); +} + /** * Convert an Immich base64-encoded ThumbHash into a tiny data URL * suitable for next/image's blurDataURL prop. */ export function thumbHashToBlurDataUrl(base64: string): string { + const cached = blurDataUrlCache.get(base64); + if (cached) return cached; + const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)); - return thumbHashToDataURL(bytes); + const url = thumbHashToDataURL(bytes); + setCache(blurDataUrlCache, base64, url); + return url; } /** @@ -20,6 +39,9 @@ export function thumbHashToBlurDataUrl(base64: string): string { * Computes the weighted average of all non-transparent pixels. */ export function thumbHashToDominantHex(base64: string): string { + const cached = dominantHexCache.get(base64); + if (cached) return cached; + const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)); const { w, h, rgba } = thumbHashToRGBA(bytes); @@ -38,11 +60,16 @@ export function thumbHashToDominantHex(base64: string): string { } } - if (total === 0) return '#888888'; + if (total === 0) { + setCache(dominantHexCache, base64, '#888888'); + return '#888888'; + } const avg = (v: number) => Math.round(v / total) .toString(16) .padStart(2, '0'); - return `#${avg(r)}${avg(g)}${avg(b)}`; + const hex = `#${avg(r)}${avg(g)}${avg(b)}`; + setCache(dominantHexCache, base64, hex); + return hex; }