Go to file
Byron Gamatos 8ef97708ef
perf(folder_library): render only the songs on screen — 1.3M DOM nodes -> ~30 (#965) (#967)
* perf(folder_library): render only the songs on screen (#965)

A song list rendered EVERY song it held. On a flat 50,944-song library that is
one <div> with 50,938 children and ~1,300,000 DOM nodes — ~4.2 GB of renderer
RSS, for a screen the user may not even be looking at (it was built while the
visible screen was v3-home).

It is not just this plugin's problem. A million-node document poisons unrelated
code: any `document.querySelector` that MISSES has to walk the whole tree before
returning null. That is exactly how song_preview's per-frame menu check ended up
consuming ~50% of the renderer and dropping the app to 2.7 fps
(feedBack-plugin-song-preview#7 fixes the per-frame walk; this fixes the tree it
was walking).

So render only what is on screen. Rows are uniform height (grid cards uniform
size), so the window is pure arithmetic — no per-row observers. Off-window songs
are represented by padding ON THE LIST rather than spacer elements: a spacer div
would become a grid ITEM in grid view and shift the columns, whereas padding
behaves identically in both layouts. Lists at or below VIRTUAL_MIN (200) render
in full exactly as before, so normal folders are untouched.

Two ordering fixes this forced, both real bugs waiting to happen:
  - Both expand handlers populated the list BEFORE showing it. A windowed list
    measures a real row and the scroller viewport, and both are zero under
    display:none. Show first, then populate.
  - _render() now tears down the previous render's scroll listeners. Without it
    they survive against detached nodes and leak on every re-render.

Verified in real Chromium over CDP with 50,000 rows — the DOM glue, not just the
maths:

    at top          rendered= 25 rows   scrollHeight=2,200,000px   [0..24]
    scroll   500k   rendered= 31 rows   scrollHeight=2,200,000px   [11357..11387]
    scroll 1,100k   rendered= 31 rows   scrollHeight=2,200,000px   [24994..25024]
    scroll to end   rendered= 25 rows   scrollHeight=2,200,000px   [49975..49999]

25-31 rows in the DOM instead of 50,000; scroll height exact and constant (the
scrollbar stays honest); the last row lands on song 49,999.

Tests: _visibleWindow is pure and exposed via __test — top/middle/bottom/past-
the-end windows, the grid row-packing case, the padding-plus-rendered-equals-
total invariant that keeps the list from changing height as you scroll, and the
degenerate zero-height case (a list still display:none) falling back to
render-everything rather than to an empty list. eslint clean; full JS suite
1186/1186.

* fix(folder_library): re-window on resize and on show/hide (PR #967 review)

CodeRabbit caught two real bugs in the first pass. Both are mine.

1. GRID RESIZE. perRow and rows were captured once when the list was filled, but
   paint() also runs on resize — and resizing changes the grid's column count.
   The window maths then sliced against the OLD column count: wrong songs on
   screen, and padding sized for a row count the layout no longer had (so the
   scrollbar lied). metrics() now recomputes perRow/itemH/rows together on every
   paint, so the geometry can never disagree with itself.

2. STALE WINDOWS ON SHOW/HIDE. paint() only ran on scroll and resize. Expanding
   or collapsing any section moves every list below it, and a windowed list's
   contents are a function of its POSITION — so those lists kept the window from
   their old position and showed blank padding where songs should be until the
   user happened to scroll. Both toggles now call _repaintVirtualLists().
   Re-opening an already-populated section had the same flaw.

   Collapsed lists also kept doing layout work on every scroll tick. paint() now
   bails early when the list is display:none or detached, and forgets its last
   window so re-showing repaints from scratch instead of short-circuiting on a
   stale memo.

Tests: grid re-window on a column-count change, the padding+rendered=rows
invariant at two different perRow values, and a test that PINS THE FAILURE MODE —
a mismatched perRow/rows pair must not silently look correct. 12/12.
Re-validated the DOM glue in real Chromium with 50k rows (25-31 rows rendered,
scroll height exact). eslint clean; JS 1189/1189; pytest 2597 passed.

CHANGELOG entry added (also flagged).
2026-07-14 21:37:22 +02:00
.claude/skills Clean release snapshot 2026-06-16 18:47:13 +02:00
.github ci: cover gap-fill manifest key scans 2026-07-13 13:25:44 +02:00
.specify refactor(ui)!: remove the classic v2 shell — v3 is the only UI (R3a) (#871) 2026-07-11 16:33:03 +02:00
content/starter Delete content/starter/beethoven-ode_to_joy.feedpak 2026-07-03 23:42:45 +02:00
data/progression Clean release snapshot 2026-06-16 18:47:13 +02:00
docs Career v3: Gold tier — verified improv upgrades an earned badge (#960) 2026-07-14 11:26:36 +02:00
lib perf(paths): resolve the library root once, not on every path check (#966) 2026-07-14 19:48:39 +02:00
plugins perf(folder_library): render only the songs on screen — 1.3M DOM nodes -> ~30 (#965) (#967) 2026-07-14 21:37:22 +02:00
scripts perf(harness): add 2D-highway frame-time measurement + capture the R3c gate (H0) (#848) 2026-07-10 23:15:07 +02:00
specs rename: slopsmith → feedBack, byron → got-feedBack (#537) 2026-06-23 11:03:01 +02:00
static feat(career): profile passport wall, home career card, shareable PNG card (#955) 2026-07-14 01:25:39 +02:00
tests perf(paths): resolve the library root once, not on every path check (#966) 2026-07-14 19:48:39 +02:00
tools fix(sloppak): the full mix is a stem — drop the invented original_audio key (#946) 2026-07-13 12:22:42 -04:00
.dockerignore fix(build): move appstate.py + routers/ under lib/ so the desktop app ships them (#836) 2026-07-10 17:16:34 +02:00
.gitattributes Purge external-format terminology from code, tests and docs 2026-06-16 19:36:53 +01:00
.gitignore feat(career): career plugin — stars, venue tiers, pack downloads (career mode 2/3) (#907) 2026-07-12 22:39:19 +02:00
build-proxmox-ct.sh rename: slopsmith → feedBack, byron → got-feedBack (#537) 2026-06-23 11:03:01 +02:00
CHANGELOG.md perf(folder_library): render only the songs on screen — 1.3M DOM nodes -> ~30 (#965) (#967) 2026-07-14 21:37:22 +02:00
CLAUDE.md ci: cover gap-fill manifest key scans 2026-07-13 13:25:44 +02:00
CONTRIBUTING.md rename: slopsmith → feedBack, byron → got-feedBack (#537) 2026-06-23 11:03:01 +02:00
docker-compose.nas.yml rename: slopsmith → feedBack, byron → got-feedBack (#537) 2026-06-23 11:03:01 +02:00
docker-compose.yml fix(build): move appstate.py + routers/ under lib/ so the desktop app ships them (#836) 2026-07-10 17:16:34 +02:00
Dockerfile fix(build): move appstate.py + routers/ under lib/ so the desktop app ships them (#836) 2026-07-10 17:16:34 +02:00
eslint.config.js refactor(highway): carve the constants into static/js/highway-constants.js (R3c) (#914) 2026-07-12 12:24:40 +02:00
feedpak-spec-exceptions.yml fix(sloppak): the full mix is a stem — drop the invented original_audio key (#946) 2026-07-13 12:22:42 -04:00
LICENSE Clean release snapshot 2026-06-16 18:47:13 +02:00
main.py rename: slopsmith → feedBack, byron → got-feedBack (#537) 2026-06-23 11:03:01 +02:00
package-lock.json R0: module-migration rails (src/ serving, live-edit cache, scriptType loading, governance) (#812) 2026-07-08 10:14:40 +02:00
package.json R0: module-migration rails (src/ serving, live-edit cache, scriptType loading, governance) (#812) 2026-07-08 10:14:40 +02:00
playwright.config.ts Clean release snapshot 2026-06-16 18:47:13 +02:00
pyproject.toml Clean release snapshot 2026-06-16 18:47:13 +02:00
requirements-test.txt Clean release snapshot 2026-06-16 18:47:13 +02:00
requirements.txt Make the library loader sloppak/loose-only 2026-06-16 19:09:53 +01:00
server.py fix(panes): harden the persisted host map against unsafe pane ids 2026-07-12 19:39:57 -04:00
SUPPORTERS.md rename: slopsmith → feedBack, byron → got-feedBack (#537) 2026-06-23 11:03:01 +02:00
tailwind.config.js refactor(ui)!: remove the classic v2 shell — v3 is the only UI (R3a) (#871) 2026-07-11 16:33:03 +02:00
TODO.md docs: correct plugin URL casing after the feedBack rename (#576) 2026-06-23 11:20:23 +02:00
uv.lock Clean release snapshot 2026-06-16 18:47:13 +02:00
VERSION Bump version to 0.3.0-alpha.1 2026-07-03 13:41:50 +02:00