perf(web): batch folder counts, bounded scroll scan, adaptive thumbs, lazy preview
Six-item frontend performance pass on the SvelteKit app.
P1 — Move per-folder photo counts to a new sidecar endpoint and defer
the fetch to requestIdleCallback. The old client-side path fired one
/photos?count=1000 per folder from the browser (≈1 MB JSON × N folders)
on every cold sidebar mount; the new POST /api/sidecar/folders/counts
fans out over loopback with bounded concurrency and returns a single
{path: count} payload of a few KB.
P2 — Bound the visibleRange scroll-scan around the previous visible
band instead of sweeping every shell from index 0 on each scroll-rAF.
Falls back to a full sweep on cache miss (filter reset, programmatic
jump) so behaviour is unchanged at the edges.
P3 — Adaptive thumbnail size + srcset. PhotoTile now picks the smallest
PhotoPrism tile_* variant (100/224/500) that covers the user's grid
preset at the current DPR. Adds decoding="async".
P4 — Lift the selection check above the {#each} loop. Mostly readability
— SvelteSet.has() is already per-key reactive — but keeps the hot loop
body terse.
P5 — Split dedupedAll / photos derivations so filter-store mutations
(search-as-you-type, section toggles) don't re-walk every loaded page;
only the cheap folder-scope filter re-runs.
P6 — Dynamic-import PreviewOverlay on first preview.uid !== null and
cache the loaded module; closing the overlay leaves the component
mounted with its internal {#if} collapsing the DOM.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -88,6 +88,55 @@ export function thumbUrl(hash: string, size = 'tile_500'): string {
|
||||
return `/api/v1/t/${hash}/${session.previewToken}/${size}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* PhotoPrism's square-cropped tile sizes (px). These are the variants
|
||||
* the indexer generates by default for the `tile_*` family. fit_* exists
|
||||
* for non-square sizing but is the wrong fit for grid cells with
|
||||
* `object-cover` — we always render a square.
|
||||
*/
|
||||
const TILE_SIZES = [100, 224, 500] as const;
|
||||
|
||||
/**
|
||||
* Pick the smallest PhotoPrism tile variant whose pixel count is at or
|
||||
* above the on-screen target. Falls through to the largest (500) for
|
||||
* anything bigger — we don't have a tile_720+ variant. Used by both the
|
||||
* 1x and 2x slots of the srcset helper below.
|
||||
*/
|
||||
function pickTileSize(targetPx: number): string {
|
||||
for (const s of TILE_SIZES) {
|
||||
if (s >= targetPx) return `tile_${s}`;
|
||||
}
|
||||
return `tile_${TILE_SIZES[TILE_SIZES.length - 1]}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a thumbnail `srcset` for a photo at a given on-screen tile
|
||||
* size. The browser picks the right variant for the current device
|
||||
* pixel ratio — on a 2x display we serve `tile_500` for a 272px XL
|
||||
* tile, on a 1x display the same tile gets `tile_500` only if no
|
||||
* smaller variant covers it, so most users save bandwidth.
|
||||
*
|
||||
* Returns the `srcset` value (no `src` attribute — pair with `thumbUrl`
|
||||
* for the 1x fallback). Two variants is enough: PhotoPrism only
|
||||
* indexes three square sizes (100/224/500), so 1x and 2x cover the
|
||||
* realistic DPR range without flooding the cache.
|
||||
*/
|
||||
export function thumbSrcSet(hash: string, targetPx: number): string {
|
||||
if (!session.previewToken) return '';
|
||||
const one = pickTileSize(targetPx);
|
||||
const two = pickTileSize(targetPx * 2);
|
||||
const url1x = thumbUrl(hash, one);
|
||||
const url2x = thumbUrl(hash, two);
|
||||
if (url1x === url2x) return `${url1x} 1x`;
|
||||
return `${url1x} 1x, ${url2x} 2x`;
|
||||
}
|
||||
|
||||
/** Companion to `thumbSrcSet` — the `src` attribute value (1x). */
|
||||
export function thumbSrc(hash: string, targetPx: number): string {
|
||||
if (!session.previewToken) return '';
|
||||
return thumbUrl(hash, pickTileSize(targetPx));
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a video stream URL. PhotoPrism's endpoint is
|
||||
* /api/v1/videos/:hash/:token/:format — same previewToken as thumbnails.
|
||||
|
||||
Reference in New Issue
Block a user