Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(web): viewport reactivity, off-screen thumbhashes being rendered #15435

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fast path for smaller date groups
mertalev committed Jan 23, 2025

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
commit 73f83fa14af365882bff00603b0f2d0d621600d5
9 changes: 3 additions & 6 deletions web/src/lib/actions/thumbhash.ts
Original file line number Diff line number Diff line change
@@ -5,14 +5,11 @@ import { decodeBase64 } from '$lib/utils';
* @param canvas
* @param param1 object containing the base64 encoded hash (base64Thumbhash: yourString)
*/
export async function thumbhash(canvas: HTMLCanvasElement, { base64ThumbHash }: { base64ThumbHash: string }) {
const ctx = canvas.getContext('2d');
export function thumbhash(canvas: HTMLCanvasElement, { base64ThumbHash }: { base64ThumbHash: string }) {
const ctx = canvas.getContext('bitmaprenderer');
if (ctx) {
const { w, h, rgba } = thumbHashToRGBA(decodeBase64(base64ThumbHash));
const bitmap = await createImageBitmap(new ImageData(rgba, w, h));
canvas.width = w;
canvas.height = h;
ctx.drawImage(bitmap, 0, 0);
void createImageBitmap(new ImageData(rgba, w, h)).then((bitmap) => ctx.transferFromImageBitmap(bitmap));
}
}

18 changes: 17 additions & 1 deletion web/src/lib/components/assets/thumbnail/thumbnail.svelte
Original file line number Diff line number Diff line change
@@ -39,6 +39,7 @@
thumbnailSize?: number | undefined;
thumbnailWidth?: number | undefined;
thumbnailHeight?: number | undefined;
eagerThumbhash?: boolean;
selected?: boolean;
selectionCandidate?: boolean;
disabled?: boolean;
@@ -71,6 +72,7 @@
thumbnailSize = undefined,
thumbnailWidth = undefined,
thumbnailHeight = undefined,
eagerThumbhash = true,
selected = false,
selectionCandidate = false,
disabled = false,
@@ -206,8 +208,22 @@
? 'bg-gray-300'
: 'bg-immich-primary/20 dark:bg-immich-dark-primary/20'}"
>
<!-- TODO: Rendering thumbhashes for offscreen assets is a ton of overhead.
This is here to ensure thumbhashes appear on the first
frame instead of a gray box for smaller date groups,
where the overhead (while wasteful) does not cause major issues. -->
{#if eagerThumbhash && !loaded && asset.thumbhash}
<canvas
use:thumbhash={{ base64ThumbHash: asset.thumbhash }}
class="absolute object-cover z-10"
style:width="{width}px"
style:height="{height}px"
out:fade={{ duration: THUMBHASH_FADE_DURATION }}
></canvas>
{/if}

{#if intersecting}
{#if !loaded && asset.thumbhash}
{#if !eagerThumbhash && !loaded && asset.thumbhash}
<canvas
use:thumbhash={{ base64ThumbHash: asset.thumbhash }}
class="absolute object-cover z-10"
4 changes: 3 additions & 1 deletion web/src/lib/components/photos-page/asset-date-group.svelte
Original file line number Diff line number Diff line change
@@ -38,7 +38,7 @@
$: dateGroups = bucket.dateGroups;

const {
DATEGROUP: { INTERSECTION_ROOT_TOP, INTERSECTION_ROOT_BOTTOM },
DATEGROUP: { INTERSECTION_ROOT_TOP, INTERSECTION_ROOT_BOTTOM, SMALL_GROUP_THRESHOLD },
} = TUNABLES;
/* TODO figure out a way to calculate this*/
const TITLE_HEIGHT = 51;
@@ -179,6 +179,7 @@
>
{#each dateGroup.assets as asset, index (asset.id)}
{@const box = dateGroup.geometry.boxes[index]}
{@const isSmallGroup = dateGroup.assets.length <= SMALL_GROUP_THRESHOLD}
<!-- update ASSET_GRID_PADDING-->
<div
use:intersectionObserver={{
@@ -217,6 +218,7 @@
disabled={$assetStore.albumAssets.has(asset.id)}
thumbnailWidth={box.width}
thumbnailHeight={box.height}
eagerThumbhash={isSmallGroup}
/>
</div>
{/each}
6 changes: 3 additions & 3 deletions web/src/lib/components/photos-page/asset-grid.svelte
Original file line number Diff line number Diff line change
@@ -828,12 +828,12 @@
class="scrollbar-hidden h-full overflow-y-auto outline-none {isEmpty ? 'm-0' : 'ml-4 tall:ml-0 mr-[60px]'}"
tabindex="-1"
use:resizeObserver={({ width, height }) => {
if (!largeBucketMode && assetStore.maxBucketAssets > LARGE_BUCKET_THRESHOLD) {
if (!largeBucketMode && assetStore.maxBucketAssets >= LARGE_BUCKET_THRESHOLD) {
largeBucketMode = true;
// Each viewport update causes each asset to re-render both the thumbhash and the thumbnail.
// Each viewport update causes each asset to re-decode both the thumbhash and the thumbnail.
// This is because the thumbnail components are destroyed and re-mounted, possibly because of the intersection observer.
// For larger buckets, this can lead to freezing and a poor user experience.
// As a mitigation, we aggressively debounce the viewport update to reduce the number of re-renders.
// As a mitigation, we aggressively debounce the viewport update to reduce the number of these events.
updateViewport = debounce(() => $assetStore.updateViewport(safeViewport), LARGE_BUCKET_DEBOUNCE_MS, {
leading: false,
trailing: true,
1 change: 1 addition & 0 deletions web/src/lib/utils/tunables.ts
Original file line number Diff line number Diff line change
@@ -53,6 +53,7 @@ export const TUNABLES = {
INTERSECTION_DISABLED: getBoolean(localStorage.getItem('DATEGROUP.INTERSECTION_DISABLED'), false),
INTERSECTION_ROOT_TOP: localStorage.getItem('DATEGROUP.INTERSECTION_ROOT_TOP') || '150%',
INTERSECTION_ROOT_BOTTOM: localStorage.getItem('DATEGROUP.INTERSECTION_ROOT_BOTTOM') || '150%',
SMALL_GROUP_THRESHOLD: getNumber(localStorage.getItem('DATEGROUP.SMALL_GROUP_THRESHOLD'), 100),
},
THUMBNAIL: {
PRIORITY: getNumber(localStorage.getItem('THUMBNAIL.PRIORITY'), 8),