mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-30 18:22:37 -04:00 
			
		
		
		
	feat(web): don't animate cached thumbnails
Thumbnails for assets always are displayed with a thumbhash which fades out over 100ms, even if the thumbnail is cached and ready immediately. This can be a bit distracting and make Immich feel 'slow', or inefficient as it feels like the thumbnails are always being reloaded. Skipping the thumbhash and animation for cached thumbnails makes it feel much more responsive.
This commit is contained in:
		
							parent
							
								
									b1aacfdbd9
								
							
						
					
					
						commit
						2bba33f834
					
				| @ -45,15 +45,4 @@ describe('Thumbnail component', () => { | ||||
|     const tabbables = getTabbable(container!); | ||||
|     expect(tabbables.length).toBe(0); | ||||
|   }); | ||||
| 
 | ||||
|   it('shows thumbhash while image is loading', () => { | ||||
|     const asset = assetFactory.build({ originalPath: 'image.jpg', originalMimeType: 'image/jpeg' }); | ||||
|     const sut = render(Thumbnail, { | ||||
|       asset, | ||||
|       selected: true, | ||||
|     }); | ||||
| 
 | ||||
|     const thumbhash = sut.getByTestId('thumbhash'); | ||||
|     expect(thumbhash).not.toBeFalsy(); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| @ -20,6 +20,7 @@ | ||||
|   import { authManager } from '$lib/managers/auth-manager.svelte'; | ||||
|   import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; | ||||
|   import { mobileDevice } from '$lib/stores/mobile-device.svelte'; | ||||
|   import { isCached } from '$lib/utils/cache'; | ||||
|   import { moveFocus } from '$lib/utils/focus-util'; | ||||
|   import { currentUrlReplaceAssetId } from '$lib/utils/navigation'; | ||||
|   import { TUNABLES } from '$lib/utils/tunables'; | ||||
| @ -75,6 +76,12 @@ | ||||
|     IMAGE_THUMBNAIL: { THUMBHASH_FADE_DURATION }, | ||||
|   } = TUNABLES; | ||||
| 
 | ||||
|   const thumbnailURL = getAssetThumbnailUrl({ | ||||
|     id: asset.id, | ||||
|     size: AssetMediaSize.Thumbnail, | ||||
|     cacheKey: asset.thumbhash, | ||||
|   }); | ||||
| 
 | ||||
|   let usingMobileDevice = $derived(mobileDevice.pointerCoarse); | ||||
|   let element: HTMLElement | undefined = $state(); | ||||
|   let mouseOver = $state(false); | ||||
| @ -313,7 +320,7 @@ | ||||
|       <ImageThumbnail | ||||
|         class={imageClass} | ||||
|         {brokenAssetClass} | ||||
|         url={getAssetThumbnailUrl({ id: asset.id, size: AssetMediaSize.Thumbnail, cacheKey: asset.thumbhash })} | ||||
|         url={thumbnailURL} | ||||
|         altText={$getAltText(asset)} | ||||
|         widthStyle="{width}px" | ||||
|         heightStyle="{height}px" | ||||
| @ -344,17 +351,31 @@ | ||||
|         </div> | ||||
|       {/if} | ||||
| 
 | ||||
|       {#if (!loaded || thumbError) && asset.thumbhash} | ||||
|         <canvas | ||||
|           use:thumbhash={{ base64ThumbHash: asset.thumbhash }} | ||||
|           data-testid="thumbhash" | ||||
|           class="absolute top-0 object-cover" | ||||
|           style:width="{width}px" | ||||
|           style:height="{height}px" | ||||
|           class:rounded-xl={selected} | ||||
|           draggable="false" | ||||
|           out:fade={{ duration: THUMBHASH_FADE_DURATION }} | ||||
|         ></canvas> | ||||
|       {#if asset.thumbhash} | ||||
|         {#await isCached(new Request(thumbnailURL))} | ||||
|           <canvas | ||||
|             use:thumbhash={{ base64ThumbHash: asset.thumbhash }} | ||||
|             data-testid="thumbhash" | ||||
|             class="absolute top-0 object-cover" | ||||
|             style:width="{width}px" | ||||
|             style:height="{height}px" | ||||
|             class:rounded-xl={selected} | ||||
|             draggable="false" | ||||
|           ></canvas> | ||||
|         {:then cached} | ||||
|           {#if !cached && !loaded && !thumbError} | ||||
|             <canvas | ||||
|               use:thumbhash={{ base64ThumbHash: asset.thumbhash }} | ||||
|               data-testid="thumbhash" | ||||
|               class="absolute top-0 object-cover" | ||||
|               style:width="{width}px" | ||||
|               style:height="{height}px" | ||||
|               class:rounded-xl={selected} | ||||
|               draggable="false" | ||||
|               out:fade={{ duration: THUMBHASH_FADE_DURATION }} | ||||
|             ></canvas> | ||||
|           {/if} | ||||
|         {/await} | ||||
|       {/if} | ||||
|     </div> | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										18
									
								
								web/src/lib/utils/cache.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								web/src/lib/utils/cache.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | ||||
| let cache: Cache | undefined; | ||||
| 
 | ||||
| const getCache = async () => { | ||||
|   cache ||= await openCache(); | ||||
|   return cache; | ||||
| }; | ||||
| 
 | ||||
| const openCache = async () => { | ||||
|   const [key] = await caches.keys(); | ||||
|   if (key) { | ||||
|     return caches.open(key); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| export const isCached = async (req: Request) => { | ||||
|   const cache = await getCache(); | ||||
|   return !!(await cache?.match(req)); | ||||
| }; | ||||
| @ -29,6 +29,6 @@ export const TUNABLES = { | ||||
|     NAVIGATE_ON_ASSET_IN_VIEW: getBoolean(storage.getItem('ASSET_GRID.NAVIGATE_ON_ASSET_IN_VIEW'), false), | ||||
|   }, | ||||
|   IMAGE_THUMBNAIL: { | ||||
|     THUMBHASH_FADE_DURATION: getNumber(storage.getItem('THUMBHASH_FADE_DURATION'), 100), | ||||
|     THUMBHASH_FADE_DURATION: getNumber(storage.getItem('THUMBHASH_FADE_DURATION'), 1000), | ||||
|   }, | ||||
| }; | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user