perf(environments): stop the picker loading full-size images - #49
Merged
rosspeili merged 2 commits intoAug 13, 2026
Merged
Conversation
The picker pointed at the same asset as the stage, so opening Appearance handed the compositor three animated GIFs — ~32MB and ~480 frames between them — to play inside 40px boxes. All three loaded whether or not one was selected, and stayed resident for the rest of the session. Built-ins now carry a committed 192x112 poster that the picker uses instead. The stage is untouched, which makes it the only thing that ever loads a GIF, and only for the environment actually in use. The posters are generated by the existing `npm run thumbs` path rather than a new dependency: no GIF decoder ships with the project and the renderer already has one, the same argument generateBundledThumbnails.js makes for avatar portraits. That also puts the image-thumbnail helper in place for the custom environment folder, which needs the same thing at runtime. Posters are globbed rather than imported by name, following config/avatars.js. A named import would deadlock the generator, which imports this module: adding an environment would fail to resolve the poster that the run was supposed to create. `loading="lazy"` comes along on the built-in row for parity with the Custom grid, though it does nothing for elements already in the viewport. The custom/ embargo test asserted that no `import.meta.glob` survived a production build, which was a proxy for "the custom glob is gone" that only held while environments.js had exactly one glob. It now asserts on the custom/ path itself, and that the thumbs glob is kept. Refs ARPAHLS#22 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
loadLibraryEnvironments pulled every file in a configured folder fully into the renderer as a blob url — no cap, no laziness, and it ran when the folder was configured rather than when the Custom expander was opened. Blob urls pin their bytes, so unlike ordinary images Chromium could not discard and refetch them: a folder of large gifs held hundreds of MB resident for the session, and the picker animated all of it inside 40px boxes. Entries now carry no image data. The picker draws a poster generated once per file and cached under userData/thumbnails/, reusing the cache and the IPC channel avatar portraits already use — keying on the absolute path means the two kinds share it without colliding. Only the environment actually selected has its bytes read. Posters are generated with ImageDecoder rather than three.js, and run three at a time rather than strictly serially: unlike a VRM parse this is decode work Chromium runs off-thread, so the cap is there to bound resident source files, not to protect the frame rate. Reading at selection time costs a delay a resident folder did not, so two things soften it. The read moved off the Electron main process, which a synchronous read of a 20MB gif blocked — window and all. And the stage holds the background it is already showing, glow included, until the new one is ready: `pending` distinguishes an image still being read from a selection that has no image, so None and Color fade still take effect at once. Bridging with the low-resolution poster was tried first and read as a fault rather than as a load. Refs ARPAHLS#22 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Collaborator
Author
Contributor
|
Thanks @AUDOSt0ck1ng, solid work as always. Merging as-is, and we can close the issue, don't need to open until we re-check the intermittent flicker, and leave stage GIF re-encode as a follow-up if it poses an imminent need. we can close the issue with a relevant comment. <3 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #22.
The environment picker pointed at the same assets as the stage, and a custom environments folder was read into memory in full the moment it was configured. This changes both. It is not framed as a fix for the flicker in #22 — I cannot reproduce that, and the decisive question there is still open — but it removes both of the things that issue identified as suspicious, and it stands on its own regardless. Rationale and measurements are in this comment.
Two commits, in order.
1. Still posters for the built-in picker thumbs
Opening Appearance handed the compositor
stars+code+bloom— ~32MB and ~480 frames — to play inside 40px boxes, and loaded all three whether or not one was selected. Built-ins now carry a committed 192×112 poster (141KB for all three) that the picker uses. The stage is untouched, so it becomes the only thing that ever loads a GIF, and only for the environment in use.Posters are generated by the existing
npm run thumbspath rather than a new dependency — no GIF decoder ships with the project and the renderer already has one, the same argumentgenerateBundledThumbnails.jsmakes for avatar portraits. They are globbed rather than imported by name, followingconfig/avatars.js: a named import would deadlock the generator, which imports that module, so adding an environment would fail to resolve the poster the run was meant to create.npm run thumbsnow regenerates both sets; docs updated accordingly.2. Read a custom folder one image at a time
loadLibraryEnvironmentsread every file in a configured folder fully into the renderer as a blob URL — no cap, no laziness, and triggered by configuring the folder rather than by opening the Custom expander. Blob URLs pin their bytes, so Chromium could not discard and refetch them the way it can an ordinary image.Entries now carry no image data. The picker draws a poster generated once per file and cached under
userData/thumbnails/, reusing the cache and IPC channel avatar portraits already use — keying on the absolute path means the two kinds share it without colliding, so the Electron side needed no new cache. Only the selected environment has its bytes read.Posters are generated with
ImageDecoder, three at a time rather than strictly serially: unlike a VRM parse this is decode work Chromium runs off-thread, so the cap bounds resident source files rather than protecting the frame rate.Reading at selection time costs a delay that a resident folder did not, so two things soften it:
fs.readFileSyncof a 20MB GIF blocked the window itself (this helps avatars and animations too, since they share the channel);pendingdistinguishes an image still being read from a selection that has no image, so None and Color fade still take effect at once.Bridging with the low-resolution poster was tried first and read as a fault rather than as a load, hence holding the previous background instead.
Behaviour changes worth calling out
Testing
npm run lint,npm test(49) andnpm run buildare green.scripts/custom-envs.test.mjsasserted that noimport.meta.globsurvived a production build, which was a proxy for "the custom/ glob is gone" that only held whileenvironments.jshad exactly one glob. It now asserts on the custom/ path itself and that the thumbs glob is kept — the embargo from #44 is unchanged and still covered.New Electron coverage for the async read. Per CONTRIBUTING, everything touching React, the DOM or
URL.createObjectURLhas no automated coverage: the built-in posters and the first cut of the custom-folder path were checked by hand on Windows 11, and the final hold-the-previous-background behaviour is verified.Not in this PR
Re-encoding or shortening the bundled GIFs (suggestion 2 on #22). Worth doing, but it changes visual assets and its benefit is stage-side RAM with no causal link to the flicker.
🤖 Generated with Claude Code