feat(ui): warm album art ahead of the scroll
Scrolling the albums grid pops art in: the cards already draw the smallest adequate tier and are already lazy, so what was left is *when* the request happens. The grids are virtualized, so the `<img>` — and therefore the fetch — does not exist until the virtualizer renders its card, which is about 1000px past the viewport, or two screens on the reference device. The issue asks for a larger overscan and that is not available: `_overhang` is a hard-coded `protected` field on `BaseLayout` with no configuration surface. So the request is issued ahead of the element instead. `utils/image-prefetch.ts` warms a bounded window either side of the rendered range, from `rangeChanged` rather than `visibilityChanged` — the two report different ranges, and a window measured from what is *visible* is spent on cards that already exist. Cover and artist URLs are served under `Cache-Control: immutable` (content-hashed filenames), so a prefetched image is a cache hit by the time its card is drawn. The bytes are the browser's; what this holds is the set of URLs asked for, capped and reported to `__yjCacheStats()`. Measured on the bulk seed (4 988 albums), ten 2 400px jumps, covers in the viewport with `naturalWidth === 0`: 254 of 258 blank one frame after the jump and 214 two frames after, against 117 and 77 with the prefetch. Closes #65
This commit is contained in:
@@ -3403,6 +3403,46 @@ rather than searching it — the store replaces that array when its
|
||||
contents change and shares the unchanged members, which is the same
|
||||
signal `track-list`'s memoized caches key on.
|
||||
|
||||
**And the right tier arriving late still reads as no art at all**, so
|
||||
the two grids ask for it before the card exists (#65).
|
||||
`utils/image-prefetch.ts` warms the images a scroll is about to reach,
|
||||
from `cover-grid`'s and `artists-view`'s virtualizers. Measured on the
|
||||
50 000-track bulk seed over ten 2 400px jumps: of 258 covers arriving
|
||||
in view, **254 were still blank one frame later and 214 two frames
|
||||
later**; with the prefetch, 117 and 77. Both builds are clean by 50 ms
|
||||
on a desktop with 3.7 kB fixture covers, which is where the reference
|
||||
device's slower engine and 27 kB covers spend their pop-in.
|
||||
|
||||
Four things about it are load-bearing.
|
||||
|
||||
**The overscan the obvious fix asks for does not exist.**
|
||||
`@lit-labs/virtualizer`'s `_overhang` is a hard-coded 1000px
|
||||
`protected` field on `BaseLayout` with no configuration surface, so
|
||||
raising it means monkey-patching a private. 1000px is about two
|
||||
screens on a 439px viewport, and the *image* cannot be requested until
|
||||
the card it lives in is rendered — which is what this asks for
|
||||
instead.
|
||||
|
||||
**It hangs off `rangeChanged`, not `visibilityChanged`.** Those report
|
||||
different ranges: visibility is what is on screen, and the virtualizer
|
||||
has already rendered that 1000px past it. Anchored to the visible
|
||||
range the window is spent on cards that already exist and have already
|
||||
asked for their own art — measured as the difference between the
|
||||
prefetch reaching one row past the last card and reaching a full
|
||||
window past it.
|
||||
|
||||
**It is not the `LRUMap` path, and saying so is the bound.** That
|
||||
ceiling holds Explore's base64 data URLs in JS; a library cover is a
|
||||
plain URL under `Cache-Control: immutable` (the filenames are content
|
||||
hashes), so what retains the bytes is the browser's own cache. What
|
||||
this module retains is the *set of URLs already asked for*, capped at
|
||||
512 and reported to `window.__yjCacheStats()` — 497 entries and 15 407
|
||||
chars after the run above.
|
||||
|
||||
**The prefetch asks for what the card will draw.** `artists-view`'s
|
||||
tier ladder moved into `artistAvatarURL()` so the two cannot disagree;
|
||||
a second copy would be a warm cache for a tier nothing renders.
|
||||
|
||||
**The same rule, on the selection path, was the worst stall in the
|
||||
app.** Five components turned selected file paths back into tracks with
|
||||
`filePaths.map(fp => tracks.find(…))`, so "Select all → Edit tags" at
|
||||
|
||||
Reference in New Issue
Block a user