Canvas UI measurements
These are historical measurements before min/max and automatic text wrapping. See finalization for the current library decision, performance, teardown profile and CI evidence.
Final measured runtime: 94065a6f5ccaa66d488df08c2e61e6d4f60b9307. Node v24.22.1, linux, AMD EPYC 9V74 80-Core Processor; Chromium 156.0.8078.4, SwiftShader WebGL and forced Canvas2D. Browser type is asserted (1/4), not inferred from pixels alone. Runs were sequential, with no concurrent local tests/builds.
Raw reports: model, browser, earlier esnext bundles, CLI-default bundle totals. Time units below are milliseconds. Node timings are not browser FPS; synthetic dispatch timings are not physical-device input latency.
Model/layout/router
Synthetic fixed text metrics, a wrapped row, 10 warmups and 100 recorded dirty-layout/gesture samples. A gesture is down + five moves + up, with one additional Click delivery. Idle values divide batches of 1000 calls and include assertion overhead. Disposal is one observation per size.
| Buttons (+1 root) | Dirty layout p50 | One text edit + layout p50 | Idle call p50 | Gesture p50 | Hit visits / gesture | Dispose |
|---|---|---|---|---|---|---|
| 10 | 0.0206 | 0.0060 | 0.0000108 | 0.0154 | 88 | 0.984 |
| 100 | 0.0535 | 0.0301 | 0.0000297 | 0.0514 | 808 | 0.964 |
| 1000 | 0.2748 | 0.2246 | 0.0000033 | 0.4777 | 8008 | 8.722 |
| 5000 | 1.4484 | 1.4077 | 0.0000277 | 2.5616 | 40008 | 45.761 |
Unchanged calls produced zero extra layouts. Paint-only style changes also skip Flex (unit regression). Eight hit queries visit 8×(N+1) nodes: there is no spatial-index or sublinear-hit-test claim. Every disposal asserts zero model nodes/subscriptions, captured/suppressed pointers and event listeners. Dirty layout can reposition siblings/percentage children, so the root is recomputed; visual signatures avoid recreating unchanged text styles/geometry.
| Nesting depth | Layout p50 | Arrange visits / layout | Intrinsic helper visits / layout | Constrained helper visits / layout |
|---|---|---|---|---|
| 16 | 0.0383 | 16 | 256 | 240 |
| 32 | 0.0712 | 32 | 1024 | 992 |
| 64 | 0.2544 | 64 | 4096 | 4032 |
Arranged-node count is not all computation. Deep nesting adds constrained measurement work; 64 levels is the supported limit. The 100×100-button model Scene-cycle forced-GC heap record includes Vite/SSR and measurement allocations, and does not establish leak freedom.
Browser synchronization and input
Viewport 1000×700, DPR1. Twenty recorded samples; five initial draw warmups and ten pointer warmups. Synchronization includes model layout, real font measurement, Pixi object/Graphics commands and native mirror updates, but excludes app.render. Draw submission is synchronous CPU/driver time, not GPU completion. All 30 synthetic gestures assert 240 deliveries and 240 UI hit tests. Functional mouse/CDP-touch/keyboard tests are separate.
| Renderer | Buttons (+3 model nodes) | Edit/layout/sync p50 | Resize/sync p50 | Idle update p50 | Gesture p50 | Draw submission p50 |
|---|---|---|---|---|---|---|
| WebGL | 10 | 0.200 | 0.150 | 0.000000 | 0.200 | 0.500 |
| WebGL | 100 | 0.300 | 0.600 | 0.000000 | 0.300 | 0.950 |
| WebGL | 1000 | 2.800 | 10.825 | 0.000000 | 2.900 | 13.950 |
| Canvas2D | 10 | 0.200 | 0.150 | 0.000000 | 0.200 | 0.400 |
| Canvas2D | 100 | 0.200 | 0.500 | 0.000000 | 0.300 | 0.700 |
| Canvas2D | 1000 | 1.400 | 6.250 | 0.000000 | 1.700 | 8.450 |
Idle updates produce zero extra layouts and zero extra visual synchronizations. Offscreen but model-visible nodes still participate in layout/traversal and retain display objects; this version does not virtualize large UI trees.
| Renderer | Buttons | Initial run (one observation) | Full Game.dispose (one observation) |
|---|---|---|---|
| WebGL | 10 | 62.300 | 689.200 |
| WebGL | 100 | 58.300 | 1347.900 |
| WebGL | 1000 | 289.500 | 2537.000 |
| Canvas2D | 10 | 18.600 | 2.100 |
| Canvas2D | 100 | 39.400 | 5.000 |
| Canvas2D | 1000 | 249.000 | 38.600 |
Full Game disposal includes Pixi renderer/context/global-resource cleanup. The large WebGL teardown times are a measured limitation in this software-rendering environment; their internal cause was not separately profiled. They must not be presented as per-Scene UI removal time.
Scene round trips and retained resources
Thirty complete 100-button UI→empty round trips per renderer; CDP GC after each five cycles. Creation/removal include Scene transaction completion and its first render. All samples return to the empty Scene’s live Pixi baseline: WebGL keeps one current world clip; Canvas2D keeps none. UI Text/Texture objects and their null slots are zero. Terminal disposal asserts zero UI presentations/nodes/buttons/text metrics/mirrors and input sources.
| Renderer | UI Scene creation p50 | UI Scene removal p50 | Initial V8 heap MiB | After 30 MiB | Change MiB |
|---|---|---|---|---|---|
| WebGL | 16.850 | 4.750 | 13.450 | 13.614 | +0.164 |
| Canvas2D | 11.050 | 4.400 | 15.013 | 15.226 | +0.213 |
This is a short forced-GC observation, not proof of no leaks. Pixi’s current world-clip null hash slots and bounded text-metric/global pools are distinct from live UI resources. Native canvas backing storage, GPU allocations, hardware rendering, physical mobile devices and multi-hour soaks were not measured.
Bundle and library selection
The CLI-default refresh at 4c9460470674e6ed18dc9fcbd688e2f0a054510d used Vite8.3.5's production minifier/default target (runtime sources unchanged from 94065a6), gzip level9 per emitted file, summed across all JS chunks (including optional renderer/fallback chunks). These totals are not initial network transfer, decoded WASM memory or installed package size. Same DOM UI/scenes entry is used before/after; Canvas trail additionally uses Sprite/Tilemap/Collision game code.
| Build | JS bytes | JS gzip bytes | CSS bytes | CSS gzip bytes |
|---|---|---|---|---|
| baseline DOM UI/scenes | 605047 | 178404 | 2027 | 887 |
| current same DOM UI/scenes | 664047 | 196733 | 2257 | 932 |
| current integrated Canvas trail | 702963 | 207412 | 2257 | 932 |
Same-entry JS increase: 59000 raw bytes, 18329 gzip bytes (17.90KiB); CSS gzip +45 bytes. No runtime layout dependency or lockfile change. The successful refresh's aggregate totals were observed before the temporary environment disconnected; its complete /tmp asset manifest was unavailable for copying. The summary records that limit explicitly; the earlier complete esnext report is retained separately.
| Micro entry | Raw JS | Gzip JS |
|---|---|---|
| empty micro entry | 23 | 43 |
| custom layout micro entry | 22281 | 7516 |
| Yoga 3.2.1 module micro entry | 126723 | 51226 |
The custom layout micro entry includes reachable Core error/module initialization code; it is not an isolated algorithm-only byte count. Yoga3.2.1 is MIT and was fetched/extracted only in /tmp, not bundled in the engine. Its ESM entry includes an asynchronous base64 WebAssembly loader. Both engines produce the same canonical padded row coordinates tested in the harness; this does not establish complete CSS equivalence or comparative speed. Yoga offers a broader, mature layout surface. The selected custom subset meets the current Row/Column/gap/padding/percentage/grow/shrink/align/justify/wrap needs with no extra runtime/ownership boundary. Reconsider it if actual games require min/max, richer intrinsic layout or CSS conformance.
Reproduce
Start from the recorded clean source, install the pinned workspace dependencies, prepare a detached baseline checkout at 89491c6 and extract yoga-layout@3.2.1. The bundle script aliases each checkout’s internal packages explicitly, so a shared node_modules cannot accidentally substitute current sources for the baseline. Run sequentially; save outputs outside the checkout until all clean-source guards have completed.
CHROMIUM_PATH=/path/to/chromium node scripts/benchmark-canvas-ui-browser.mjs /tmp/ui-browser.json
node --expose-gc scripts/benchmark-canvas-ui.mjs /tmp/ui-model.json
node scripts/measure-ui-bundles.mjs /tmp/simpludify-ui-base /tmp/simpludify-yoga/package /tmp/ui-bundles.json
Preserved investigation evidence
- Initial unvalidated run: synthetic pointer client geometry was wrong; do not use those input timings. It exposed owned GraphicsContext retention.
- Explicit-context correction: pointer assertions pass, but Canvas2D destroyed Graphics remain in a pinned Pixi hash.
- Graphics-object correction: live UI resources return to baseline; UID-keyed text null metadata remains.
- Text-key correction: text/texture slots return to zero; the final report above adds cancellation/lifetime fixes and Scene timing.
The engine isolates pinned Pixi8.22 cleanup in packages/web/src/pixi-ui-cleanup.ts. It removes only destroyed owned Graphics and null keys belonging to replaced/disposed UI Text; it never purges a live/shared image or the global cache. Dependency upgrades must rerun the resource regression suite. Custom fonts must be ready before tree construction; automatic web-font cache invalidation is outside this release. See supported contracts and validation.