Add a ?pi=1 profile, and stop re-deriving search keys per keystroke

Two separate costs, both measured on musicdolphin (Pi 4, 1920x1080 kiosk), where the
app was burning ~70% of a core with nothing happening on screen.

Per-frame work. The ambient canvas repaints a full-screen gradient plus a particle
field every frame, and `usePlaybackClock` pushes a React setState per animation frame
into both PlayView and PlayerBar for the whole length of a track. `?pi=1` (lib/
lowPower.ts) makes those cheaper rather than switching them off: the canvas paints a
quarter of the pixels at 30fps with a bubble cap, and the clock renders ten times a
second - a progress bar advances one pixel every few hundred ms and its label has
one-second resolution, so nothing on screen can tell. Only the effects with no cheap
version actually go: the backdrop-filter glass blur and the decorative CSS loops.
Also drops a redundant full-canvas clearRect that the opaque gradient always covered.

Search. normalize() runs a Unicode NFD decomposition, and albumMatches/songMatches
called it on every album title and every track title on every keystroke - 4969 of them
for a track search, whose answer cannot change until the library does. buildSearchIndex
does it once per library payload; a keystroke is now String.includes over strings that
already exist. On the real library that is 33ms -> 3.4ms for an eight-letter track
query on a laptop, and this runs on a Pi. The same index partitions albums by shelf and
pre-sorts each shelf's categories, which App and BrowseView were deriving separately
from the same data.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-20 13:15:23 +02:00
parent a7fb56c9fe
commit cc3db44c4e
11 changed files with 457 additions and 67 deletions

View File

@@ -11,6 +11,7 @@
import type { CSSProperties } from "react";
import { LOW_POWER } from "./lowPower";
import type { Group } from "./search";
import type { UiState } from "./keyboard";
@@ -36,20 +37,26 @@ export const ANIMATE_VIEW_TRANSITIONS = true;
/** Frost the glass panels/cards/rows with a real backdrop blur. `backdrop-filter` is
* one of the most GPU-expensive CSS effects in the app - a live backdrop copy+blur
* per element, repeated for every album card in the unvirtualized grid - so turn
* this off on weak hardware (e.g. an old Raspberry Pi) to fall back to a plain
* translucent background with no blur. */
export const SHOW_GLASS_BLUR = true;
* per element, repeated for every album card in the unvirtualized grid - and unlike
* the canvas there is no cheaper version of it to fall back to, so `?pi=1` switches
* it off outright: a plain translucent background, no blur. This is the one visible
* cost of the Pi profile. */
export const SHOW_GLASS_BLUR = !LOW_POWER;
/** Render the play view's animated canvas background (gradient + bubbles, both
* redrawn every frame at 60fps). Off falls back to `.stage`'s own static CSS
* gradient, which is still underneath the canvas either way. */
* gradient, which is still underneath the canvas either way.
*
* Deliberately still on under `?pi=1`: it is the app's whole look, and
* `AMBIENCE_QUALITY` in lib/lowPower.ts makes it affordable (quarter of the pixels,
* half the frames) rather than making it go away. */
export const SHOW_AMBIENCE = true;
/** Run the purely decorative CSS animation loops: the room page's bubble field and
* the dolphin mascot's bob/swim. Individually cheap (opacity/transform only) but
* free to cut on weak hardware. */
export const SHOW_DECORATIVE_ANIMATIONS = true;
* the dolphin mascot's bob/swim. Individually cheap (opacity/transform only), but
* a couple of dozen elements animating forever still means a repaint every frame on
* a machine compositing in software, and they carry no information. */
export const SHOW_DECORATIVE_ANIMATIONS = !LOW_POWER;
// ------------------------------------------------------------------ colors --