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:
@@ -364,33 +364,32 @@ export async function getImportInfo(): Promise<ImportInfo> {
|
||||
|
||||
/**
|
||||
* Per-folder photo count for each `paths[]` entry. PhotoPrism's `/folders`
|
||||
* endpoint reports `FileCount: 0` even when populated, and the `/photos`
|
||||
* response has no total-rows header — X-Count is the per-page row count.
|
||||
* So we fire one `/photos?q=path:X&count=1000` per folder and dedupe by
|
||||
* UID — `merged=false` returns one row per FILE, so a HEIC+JPG companion
|
||||
* pair counts twice if we trusted `data.length`. Capped at the server's
|
||||
* 1000-row ceiling; folders that overflow render as "1000+" in the UI.
|
||||
* endpoint reports `FileCount: 0` even when populated, so the count has
|
||||
* to be derived from a `/photos?q=path:X` lookup per folder.
|
||||
*
|
||||
* We hand this off to the sidecar (`POST /api/sidecar/folders/counts`)
|
||||
* which fans out to PhotoPrism over loopback, dedupes by UID, and
|
||||
* returns a single `{path: count}` payload of <5 KB. The previous
|
||||
* client-side implementation issued one `/photos?count=1000` per folder
|
||||
* from the browser — on a library with 30 folders that's ≈30 MB of JSON
|
||||
* pulled across the wire on every cold sidebar mount.
|
||||
*
|
||||
* `path:X` is non-recursive in PhotoPrism's q-DSL: it matches direct
|
||||
* children only, so summing the per-path counts (no double-counting from
|
||||
* nested folders) is the right way to derive the root-folder photo
|
||||
* count.
|
||||
* children only, so summing the per-path counts (no double-counting
|
||||
* from nested folders) is the right way to derive the root-folder
|
||||
* photo count. Capped at PhotoPrism's 1000-row ceiling; folders larger
|
||||
* than that under-report (pre-existing limitation, unchanged here).
|
||||
*
|
||||
* Returns a plain object keyed by the input paths to keep it JSON-friendly
|
||||
* for TanStack's structural sharing.
|
||||
* Returns a plain object keyed by the input paths to keep it JSON-
|
||||
* friendly for TanStack's structural sharing.
|
||||
*/
|
||||
export async function listFolderCounts(paths: string[]): Promise<Record<string, number>> {
|
||||
const entries = await Promise.all(
|
||||
paths.map(async (path) => {
|
||||
const { data } = await http.get<PpPhoto[]>('/photos', {
|
||||
params: { count: 1000, offset: 0, merged: false, q: `path:${path}` }
|
||||
});
|
||||
const uids = new Set<string>();
|
||||
for (const p of data) uids.add(p.UID);
|
||||
return [path, uids.size] as const;
|
||||
})
|
||||
);
|
||||
return Object.fromEntries(entries);
|
||||
if (paths.length === 0) return {};
|
||||
const data = (await sidecar('POST', '/folders/counts', { paths })) as Record<
|
||||
string,
|
||||
number
|
||||
>;
|
||||
return data;
|
||||
}
|
||||
|
||||
// ── Geo ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user