Downscale cached cover art, and stop every animation loop under ?pi=1
Covers. Art out of an ID3 APIC frame is sized for a record sleeve: this library averaged 3000x3000 and 580 kB per cover, 140 MB across 284 albums. The browser was decoding nine megapixels - around 36 MB of bitmap - for every cover it painted, to show it in a 185 px card, on a Pi with 2 GB of RAM. The largest any screen in this app asks for is 340 px (the play view), so cache.store_cover now downscales to a 640 px long edge, which leaves room for a tablet at devicePixelRatio 2 and cuts the decode about twentyfold. Pillow was already a hard dependency, for colour extraction. scan_library reuses an album whose fingerprint is unchanged without re-reading its tags, so covers already on disk would never be rewritten - hence shrink_stored_covers(), a pass at the top of a scan. Reading a JPEG's dimensions only parses its header, so after the first run it costs one small read per album. Art already small enough is returned byte-identical rather than re-encoded, so repeated scans cannot slowly grind it down, and anything Pillow cannot read is passed through untouched: a cover that is too big is a performance problem, a cover that is missing is a visible one. Animation. Halving the ambient canvas to a quarter of the pixels at 30fps took it from 53.5% of a core to 25%, and 25% was still not good enough to use. A requestAnimationFrame loop repainting the viewport is a floor you cannot get under while it runs at all, so ?pi=1 now switches it off outright rather than thinning it, along with the decorative CSS loops, the view transitions, the typing game's bubbles and its next-key pulse. The pets stay on screen but hold still, through the same path prefers-reduced-motion already took - taking the animation away is the point, taking away what she earned is not. .stage keeps its own static gradient, so there is still a sea behind everything. Both blurs on the panels stay on. Dropping those measured five times worse. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -95,34 +95,35 @@ seeking is the one thing a real player can do that the mockup could not.
|
||||
|
||||
## The Raspberry Pi profile
|
||||
|
||||
`?pi=1` loads the same app with the ambient canvas painting a quarter of the pixels. It
|
||||
exists for musicdolphin - a Pi 4 driving a 1920x1080 kiosk screen - where Firefox
|
||||
rasterizes 2D canvas in the content process, and the canvas repaints the whole screen
|
||||
every frame whether or not anything is playing.
|
||||
`?pi=1` loads the same app with nothing on screen animating itself. It exists for
|
||||
musicdolphin - a Pi 4 driving a 1920x1080 kiosk screen.
|
||||
|
||||
Everything in `src/lib/lowPower.ts` was measured on the device. Sum of the Firefox
|
||||
process tree, idle on the browse screen, 15 s average:
|
||||
| | Full | `?pi=1` |
|
||||
|---|---|---|
|
||||
| Ambient canvas (gradient + bubbles) | on | **not rendered** |
|
||||
| Decorative CSS loops, view transitions | on | off |
|
||||
| Typing game bubbles, next-key pulse | on | off |
|
||||
| Typing game pets | swimming | placed, held still |
|
||||
| `backdrop-filter` on cards and list rows | on | off |
|
||||
| `backdrop-filter` on panels | on | **on** |
|
||||
| Progress-bar re-renders | every animation frame | 10 per second |
|
||||
|
||||
| | idle CPU |
|
||||
|---|---|
|
||||
| full app | 53.5% |
|
||||
| + ambient canvas at half resolution | **25.0%** |
|
||||
| + 30 fps cap on top of that | 25.0% |
|
||||
| + decorative CSS animation loops off | 24.6% |
|
||||
| + `backdrop-filter` glass blur off | **118.1%** |
|
||||
Two of those rows are worth knowing the reasons for, because both are the opposite of
|
||||
what they look like.
|
||||
|
||||
Two things to take from that table. The canvas resolution is the whole win, and
|
||||
**turning off the backdrop blur makes it five times worse** - the blur is what promotes
|
||||
each glass panel to its own compositing layer, so a canvas frame underneath repaints
|
||||
only the canvas. Without it the canvas and the album grid above it share one layer and
|
||||
every frame repaints all of it. The most expensive-looking CSS in the app is what keeps
|
||||
the rest of it cheap; `SHOW_GLASS_BLUR` stays on everywhere, and so do the decorative
|
||||
loops, which measured as noise.
|
||||
**The panel blur stays on.** Dropping it measured *five times worse* on the device -
|
||||
118% of a core against 25%. `backdrop-filter` is what promotes each glass panel to its
|
||||
own compositing layer; without it the panels and everything under them collapse into
|
||||
one layer that repaints wholesale. The card blur goes because that is a different
|
||||
problem: three hundred of them in one grid, not one per panel.
|
||||
|
||||
So the profile is one real change plus three caps that only bite while something is
|
||||
playing, and are therefore not in the table: 30 fps on the canvas, 60 bubbles alive at
|
||||
once, and ten progress-bar re-renders a second instead of sixty (`usePlaybackClock`
|
||||
pushes a React `setState` per animation frame, into both `PlayView` and `PlayerBar`).
|
||||
**The canvas goes away rather than getting cheaper.** Half resolution and 30 fps took
|
||||
it from 53.5% of a core to 25.0%, and 25% was still not good enough to use. A
|
||||
`requestAnimationFrame` loop repainting the viewport is a floor you cannot get under
|
||||
while it runs at all. `.stage` keeps its own static CSS gradient, so there is still a
|
||||
sea behind everything - it just no longer moves or follows the track.
|
||||
`AMBIENCE_QUALITY` in `src/lib/lowPower.ts` is kept accurate for whoever turns the
|
||||
canvas back on.
|
||||
|
||||
The flag is read once at module load from the URL and nowhere else - no persistence, no
|
||||
auto-detection. That is what makes "is this the profile or the hardware?" answerable by
|
||||
|
||||
Reference in New Issue
Block a user