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

@@ -18,7 +18,7 @@ import {
unitLabel,
} from "../lib/covers";
import { clock } from "../lib/format";
import type { Category, Group, Results, SongHit } from "../lib/search";
import type { Category, Group, Results, SearchIndex, SongHit } from "../lib/search";
import { categoryMatches, groupOf } from "../lib/search";
import {
ANIMATE_VIEW_TRANSITIONS,
@@ -37,7 +37,7 @@ import { Cover } from "./Cover";
interface Props {
results: Results;
group: Group | null;
albums: Album[];
index: SearchIndex;
mode: "albums" | "tracks";
search: string;
category: string | null;
@@ -76,7 +76,7 @@ const GROUP_SECTION_LABEL: Record<Group, string> = {
export function BrowseView({
results,
group,
albums,
index,
mode,
search,
category,
@@ -114,14 +114,14 @@ export function BrowseView({
GROUPS.map((shelfGroup) => ({
group: shelfGroup,
categories: categoryMatches({
albums,
index,
search: "",
mode: "albums",
category: null,
group: shelfGroup,
}),
})),
[albums],
[index],
);
// Keep the selection on screen as the arrow keys walk past the fold.