Scrolling the albums grid pops the art in (#65). Two of the three
things the Direction asks for turned out to be checks rather than
changes — cover-grid.getCoverUrl() and artists-view's avatar
already pick _sm/_md/_lg from the card size and DPR, and both are
already loading="lazy" decoding="async". The third, "ask the
virtualizer for a larger overscan", is not available: @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.
What the overhang stands in for is the real problem: the <img> — and
therefore the request — does not exist until the virtualizer renders
its card, and 1000px is about two screens on the reference device's
439px viewport.
What changed, and why
frontend/src/utils/image-prefetch.ts warms a bounded window of images
either side of the rendered range, hung off cover-grid's and artists-view's virtualizers.
Four things in it are load-bearing:
rangeChanged, not visibilityChanged. They report different
ranges: visibility is what is on screen, and the virtualizer has
already rendered ~1000px past it. Anchored to visibility the window
is spent on cards that already exist and have already asked for their
own art. The existing visibilityChanged handlers are untouched —
they want the first visible row, and that is correct.
The bound is the URL record, not an image cache. The Findings warn
about the LRUMap ceilings; those hold Explore's base64 data URLs. A
library cover is a plain URL under Cache-Control: public, max-age=31536000, immutable (content-hashed filenames), so the bytes
are the browser's cache. What this module retains is the set of URLs
already asked for, capped at 512 and registered with cache-stats,
which is the __yjCacheStats() measurement the issue asks for.
The prefetch asks for the tier the card will draw. artistAvatarURL() is split out of renderArtistAvatar so the two
cannot disagree; a second copy of the ladder would warm a tier
nothing renders.
An in-flight image is held until it loads or fails. new Image().src = url and drop it usually survives, but "usually" is an
engine detail and the engine that matters here is Chrome 113.
Verification
Measured against the running app, make dev-headless SEED=bulk
(50 000 tracks, 4 988 albums), ten 2 400px jumps of the grid's
scroller, counting covers intersecting the viewport with naturalWidth === 0:
build
blank at frame 1
at frame 2
at 50 ms
main
254 / 258
214 / 258
0
main, second run
254 / 258
190 / 258
0
this branch
117 / 258
77 / 258
0
this branch, second run
118 / 258
96 / 258
0
Fresh browser session per row (a second run in the same session
measures the HTTP cache, not the build) and a rebuild between
builds, since the frontend is embedded and a stash alone re-runs the
old bundle. The first attempt at this measurement waited 220 ms after
each jump and found zero blanks on either build — the metric only
discriminates at one and two frames, which is where a pop-in lives.
window.__yjCacheStats().imagePrefetch after that run: 497
entries, 15 407 chars, cap 512. The ceiling holds and is checkable.
make ui-test — 1104 passed, 12 of them new
(test/utils/image-prefetch.test.ts, test/components/art-prefetch.test.ts). Proved non-vacuous: with
the two @rangeChanged bindings removed, the two component cases
that assert a row past the rendered range fail.
make e2e — 239 passed against make dev-headless SEED=default
(chromium; WebKit is CI's half). As a regression check: no new spec,
because the fixture library is eight albums and nothing scrolls
there.
npx tsc --noEmit, make css-check, make skill-check — clean. No
Go, no .sql and no .templ changed, so make lint, make test, make generate and make bindings were not run.
Deliberately not done
No device tier. The numbers above are a desktop engine against
the bulk library's 3.7 kB covers, which is why both builds are clean
by 50 ms there; the phone's own magnitude is unmeasured, though the
direction is not.
fetchpriority / eager loading for the first screenful, the
issue's third suggestion. The first screenful renders on mount and
asks for its art immediately; that is a different question from the
one reported.
The split grid's "after" virtualizer gets no hook. Its indices
are into a second array and it carries no visibilityChanged either;
it exists only while an album dropdown is expanded, which is the
state where the user has stopped scrolling.
Why this issue and not a higher one
Six PRs from earlier runs are open against a main that has not moved,
so this run avoided another branch over the same files. #200 is queue-panel, menu-surface and index.ts and #39 is track-list —
both entirely inside files #208/#209/#211/#214 already edit. #85 opens
with "re-measure before building", which needs a real tagged library
and MusicBrainz. Said on the issue at claim time.
## What the issue was
Scrolling the albums grid pops the art in (#65). Two of the three
things the Direction asks for turned out to be checks rather than
changes — `cover-grid.getCoverUrl()` and `artists-view`'s avatar
already pick `_sm`/`_md`/`_lg` from the card size and DPR, and both are
already `loading="lazy" decoding="async"`. The third, "ask the
virtualizer for a larger overscan", **is not available**:
`@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.
What the overhang stands in for is the real problem: the `<img>` — and
therefore the request — does not exist until the virtualizer renders
its card, and 1000px is about two screens on the reference device's
439px viewport.
## What changed, and why
`frontend/src/utils/image-prefetch.ts` warms a bounded window of images
either side of the rendered range, hung off `cover-grid`'s and
`artists-view`'s virtualizers.
Four things in it are load-bearing:
- **`rangeChanged`, not `visibilityChanged`.** They report different
ranges: visibility is what is on screen, and the virtualizer has
already rendered ~1000px past it. Anchored to visibility the window
is spent on cards that already exist and have already asked for their
own art. The existing `visibilityChanged` handlers are untouched —
they want the first *visible* row, and that is correct.
- **The bound is the URL record, not an image cache.** The Findings warn
about the `LRUMap` ceilings; those hold Explore's base64 data URLs. A
library cover is a plain URL under `Cache-Control: public,
max-age=31536000, immutable` (content-hashed filenames), so the bytes
are the browser's cache. What this module retains is the set of URLs
already asked for, capped at 512 and registered with `cache-stats`,
which is the `__yjCacheStats()` measurement the issue asks for.
- **The prefetch asks for the tier the card will draw.**
`artistAvatarURL()` is split out of `renderArtistAvatar` so the two
cannot disagree; a second copy of the ladder would warm a tier
nothing renders.
- **An in-flight image is held** until it loads or fails. `new
Image().src = url` and drop it usually survives, but "usually" is an
engine detail and the engine that matters here is Chrome 113.
## Verification
- **Measured against the running app**, `make dev-headless SEED=bulk`
(50 000 tracks, 4 988 albums), ten 2 400px jumps of the grid's
scroller, counting covers intersecting the viewport with
`naturalWidth === 0`:
| build | blank at frame 1 | at frame 2 | at 50 ms |
|---|---|---|---|
| `main` | 254 / 258 | 214 / 258 | 0 |
| `main`, second run | 254 / 258 | 190 / 258 | 0 |
| this branch | 117 / 258 | 77 / 258 | 0 |
| this branch, second run | 118 / 258 | 96 / 258 | 0 |
Fresh browser session per row (a second run in the same session
measures the HTTP cache, not the build) and a **rebuild** between
builds, since the frontend is embedded and a stash alone re-runs the
old bundle. The first attempt at this measurement waited 220 ms after
each jump and found zero blanks on either build — the metric only
discriminates at one and two frames, which is where a pop-in lives.
- `window.__yjCacheStats().imagePrefetch` after that run: **497
entries, 15 407 chars, cap 512**. The ceiling holds and is checkable.
- `make ui-test` — **1104 passed**, 12 of them new
(`test/utils/image-prefetch.test.ts`,
`test/components/art-prefetch.test.ts`). **Proved non-vacuous**: with
the two `@rangeChanged` bindings removed, the two component cases
that assert a row past the rendered range fail.
- `make e2e` — **239 passed** against `make dev-headless SEED=default`
(chromium; WebKit is CI's half). As a regression check: no new spec,
because the fixture library is eight albums and nothing scrolls
there.
- `npx tsc --noEmit`, `make css-check`, `make skill-check` — clean. No
Go, no `.sql` and no `.templ` changed, so `make lint`, `make test`,
`make generate` and `make bindings` were not run.
## Deliberately not done
- **No device tier.** The numbers above are a desktop engine against
the bulk library's 3.7 kB covers, which is why both builds are clean
by 50 ms there; the phone's own magnitude is unmeasured, though the
direction is not.
- **`fetchpriority` / eager loading for the first screenful**, the
issue's third suggestion. The first screenful renders on mount and
asks for its art immediately; that is a different question from the
one reported.
- **The split grid's "after" virtualizer** gets no hook. Its indices
are into a second array and it carries no `visibilityChanged` either;
it exists only while an album dropdown is expanded, which is the
state where the user has stopped scrolling.
## Why this issue and not a higher one
Six PRs from earlier runs are open against a `main` that has not moved,
so this run avoided another branch over the same files. #200 is
`queue-panel`, `menu-surface` and `index.ts` and #39 is `track-list` —
both entirely inside files #208/#209/#211/#214 already edit. #85 opens
with "re-measure before building", which needs a real tagged library
and MusicBrainz. Said on the issue at claim time.
Closes #65
CI is green on 32aa0d9 — run 17767: check success (lint, three Go build configurations, tsc, make ui-test, bindings-check, skill-check, commit-check) and e2e
success, which is the run that carries WebKit — the browser this
worktree cannot run, and the only tier here that had not seen the
change.
**CI is green** on `32aa0d9` — run
[17767](https://git.ljones.me/yonlu/yellowjacket/actions/runs/17767):
`check` success (lint, three Go build configurations, `tsc`,
`make ui-test`, bindings-check, skill-check, commit-check) and `e2e`
success, which is the run that carries **WebKit** — the browser this
worktree cannot run, and the only tier here that had not seen the
change.
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
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What the issue was
Scrolling the albums grid pops the art in (#65). Two of the three
things the Direction asks for turned out to be checks rather than
changes —
cover-grid.getCoverUrl()andartists-view's avataralready pick
_sm/_md/_lgfrom the card size and DPR, and both arealready
loading="lazy" decoding="async". The third, "ask thevirtualizer for a larger overscan", is not available:
@lit-labs/virtualizer's_overhangis a hard-coded 1000pxprotectedfield onBaseLayoutwith no configuration surface, soraising it means monkey-patching a private.
What the overhang stands in for is the real problem: the
<img>— andtherefore the request — does not exist until the virtualizer renders
its card, and 1000px is about two screens on the reference device's
439px viewport.
What changed, and why
frontend/src/utils/image-prefetch.tswarms a bounded window of imageseither side of the rendered range, hung off
cover-grid's andartists-view's virtualizers.Four things in it are load-bearing:
rangeChanged, notvisibilityChanged. They report differentranges: visibility is what is on screen, and the virtualizer has
already rendered ~1000px past it. Anchored to visibility the window
is spent on cards that already exist and have already asked for their
own art. The existing
visibilityChangedhandlers are untouched —they want the first visible row, and that is correct.
about the
LRUMapceilings; those hold Explore's base64 data URLs. Alibrary cover is a plain URL under
Cache-Control: public, max-age=31536000, immutable(content-hashed filenames), so the bytesare the browser's cache. What this module retains is the set of URLs
already asked for, capped at 512 and registered with
cache-stats,which is the
__yjCacheStats()measurement the issue asks for.artistAvatarURL()is split out ofrenderArtistAvatarso the twocannot disagree; a second copy of the ladder would warm a tier
nothing renders.
new Image().src = urland drop it usually survives, but "usually" is anengine detail and the engine that matters here is Chrome 113.
Verification
Measured against the running app,
make dev-headless SEED=bulk(50 000 tracks, 4 988 albums), ten 2 400px jumps of the grid's
scroller, counting covers intersecting the viewport with
naturalWidth === 0:mainmain, second runFresh browser session per row (a second run in the same session
measures the HTTP cache, not the build) and a rebuild between
builds, since the frontend is embedded and a stash alone re-runs the
old bundle. The first attempt at this measurement waited 220 ms after
each jump and found zero blanks on either build — the metric only
discriminates at one and two frames, which is where a pop-in lives.
window.__yjCacheStats().imagePrefetchafter that run: 497entries, 15 407 chars, cap 512. The ceiling holds and is checkable.
make ui-test— 1104 passed, 12 of them new(
test/utils/image-prefetch.test.ts,test/components/art-prefetch.test.ts). Proved non-vacuous: withthe two
@rangeChangedbindings removed, the two component casesthat assert a row past the rendered range fail.
make e2e— 239 passed againstmake dev-headless SEED=default(chromium; WebKit is CI's half). As a regression check: no new spec,
because the fixture library is eight albums and nothing scrolls
there.
npx tsc --noEmit,make css-check,make skill-check— clean. NoGo, no
.sqland no.templchanged, somake lint,make test,make generateandmake bindingswere not run.Deliberately not done
the bulk library's 3.7 kB covers, which is why both builds are clean
by 50 ms there; the phone's own magnitude is unmeasured, though the
direction is not.
fetchpriority/ eager loading for the first screenful, theissue's third suggestion. The first screenful renders on mount and
asks for its art immediately; that is a different question from the
one reported.
are into a second array and it carries no
visibilityChangedeither;it exists only while an album dropdown is expanded, which is the
state where the user has stopped scrolling.
Why this issue and not a higher one
Six PRs from earlier runs are open against a
mainthat has not moved,so this run avoided another branch over the same files. #200 is
queue-panel,menu-surfaceandindex.tsand #39 istrack-list—both entirely inside files #208/#209/#211/#214 already edit. #85 opens
with "re-measure before building", which needs a real tagged library
and MusicBrainz. Said on the issue at claim time.
Closes #65
CI is green on
32aa0d9— run17767:
checksuccess (lint, three Go build configurations,tsc,make ui-test, bindings-check, skill-check, commit-check) ande2esuccess, which is the run that carries WebKit — the browser this
worktree cannot run, and the only tier here that had not seen the
change.
32aa0d98ceto3479ae8d39