Repository navigation
Conversation
The preview GLB embedded every texture at full size (35-70 MB, up to 4096 px), and GLTFLoader was forced onto <img> decoding, which decodes synchronously on the main thread at the first render: drifter took 1.4 s to load with a 1.3 s UI freeze, after a 1 s export on a cache miss. - Export pose and rigged GLBs with `--max-texture 1024` (drifter: 39 -> 14 MB). Pose cache v15 and rigged v8 so old full-size GLBs regenerate. - The hero viewer decodes with ImageBitmap, off the main thread; frames render pixel-identical to the <img> path. Other loads (soul-container grid, import modals) keep <img> since ImageBitmaps pin their pixels in renderer memory and the grid shows many models at once. - The <img> fallback is a per-parser GLTFLoader plugin instead of hiding createImageBitmap globally, which would break a concurrent ImageBitmap load. Scene disposal now closes ImageBitmaps and frees every texture a material holds (sheen maps were missed). In app: 0.72 s cold, ~0.6 s cached, longest main-thread block ~120 ms. Needs a vpkmerge release with `--max-texture` (Slush97/vpkmerge#48) before the bundled v0.19.1 binary is bumped.
Slush97
force-pushed
the
perf/hero-preview-load
branch
from
October 4, 2026 11:31
215fb6d to
8728c1a
Compare
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.
What
Opening a hero's live 3D model took ~1.4 s even with the GLB cached, and the UI froze for ~1.3 s of it. On a cache miss it took ~2.4 s. There were two causes:
<img>decoding.loadGltfPreviewhidcreateImageBitmap, so GLTFLoader fell back to<img>elements, and Chromium decodes those synchronously on the main thread when the first render uploads them.Changes:
--max-texture 1024, so each texture embeds at its largest mip that fits (drifter skin: 39 MB -> 14 MB).POSE_PIPELINE_VERSION15 /RIGGED_PIPELINE_VERSION8 so cached full-size GLBs regenerate.HeroPoseViewerloads with{ imageBitmaps: true }, which decodes off the main thread. Everything else (soul-container grid and tiles, import modals) keeps<img>: ImageBitmaps pin their decoded pixels in renderer memory, while Chromium can drop an<img>'s, and the grid shows many models at once.<img>fallback is now a per-loader GLTFLoader plugin that swaps that parser's texture loader. HidingcreateImageBitmapglobally would break a concurrent ImageBitmap load.disposeTexturecloses ImageBitmaps (three.js never does).materialTexturesfrees every texture a material holds; the old fixed list missed the sheen maps.Measured
In the dev app, driven over CDP (drifter + pak09 skin): 0.72 s cold (export + load), ~0.6 s cached, longest main-thread block ~120 ms.
Electron 35 harness, same GLB, load to first frame:
<img><img>/ bitmap)<img>and ImageBitmap frames are pixel-identical at every cap (read back from the framebuffer).Tradeoff: at the closest zoom, 1024 is slightly soft on fine print (drifter's hat card, coat damask). 2048 looks identical to full size but costs ~300 MB more renderer memory with ImageBitmaps. It's one constant:
PREVIEW_MAX_TEXTURE.Before merging
--max-texture(feat(model): parallelize GLB texture export and add --max-texture vpkmerge#48), then aVPKMERGE_VERSIONbump inscripts/fetch-vpkmerge.mjs. The bundled v0.19.1 rejects the flag, so packaged builds would fall back to the 2D portrait for every hero. Dev builds use../vpkmerge/target/releaseand work now.heroPoseModels.tsandHeroPoseViewer.tsx, so expect a rebase on whichever lands second.Checks
tsc -b, eslint on touched files, andvitest run(1785 tests) pass.loadGltfPreview.test.tsnow covers the plugin swap, the ImageBitmap opt-in,materialTexturesanddisposeTexture.