Skip to content

fix: install the visible override once and keep it - #134

Open
rynwhite wants to merge 1 commit into
pixijs:mainfrom
rynwhite:fix/visible-override-once
Open

rynwhite wants to merge 1 commit into
pixijs:mainfrom
rynwhite:fix/visible-override-once

Conversation

@rynwhite

@rynwhite rynwhite commented Oct 14, 2026 •

Copy link
Copy Markdown

Summary

We build audio plugin editors on Pixi and @pixi/layout that run in native web views inside DAWs, so mount time matters to us: editors are opened and closed constantly while people work. We have been running a series of measured performance passes on our UI library, profiling and comparing before and after each change, and one of the biggest costs turned out to be inside @pixi/layout.

Headline: installing the visible override once instead of on every first layout made mounting 2,041 layout nodes about 73% faster in a standalone benchmark (82 ms to 23 ms in Chrome), and cut total mount CPU by about 15% in our own UI stress test. While testing the fix we also found a small visibility bug in the same code, fixed here too.

Detailed benchmarks, method and caveats are further down.

Overview

The layout setter in ContainerMixin redefines visible on Container.prototype each time a container gets its first layout, and restores Pixi's original descriptor whenever any container's layout is removed. This PR installs the override once and keeps it.

Problem

  1. Performance: mounting N layout nodes redefines an accessor N times on the prototype that every Pixi container shares. Here that adds about 30-50 µs per layout node, and the total grows linearly with N (checked from 511 to 8,161 nodes).
  2. Correctness: removing one container's layout restores the original descriptor for every container. Until some container gets a first layout again, hiding a layout child only detaches its Yoga node when the LayoutSystem's size pass next runs, and showing it again does not re-attach the node, so its siblings are laid out without it and can overlap it. The next first layout reinstalls the override but does not re-attach that node.

Fix

  • Install the override once, guarded by a module-level flag.
  • Do not restore the original descriptor on removal. The installed setter calls Pixi's setter and only does layout work when this.layout is set, so containers without a layout behave as before.

Most of the diff is re-indentation. If you would rather install the override at import time, as TextMixin does for text, I can switch to that.

Tests

Two new tests in tests/__tests__/visibility.test.ts:

  • the visible setter stays the same function across several first layouts and after one layout is removed;
  • after an unrelated container's layout is removed, hiding a layout child removes its Yoga node and showing it again re-adds it.

Both fail on main and pass with this change. Lint, types and npm run build pass. Unit tests: 123 of 127 pass; the 4 failures are story screenshot tests that load images from fakeimg.ryd.tools, which timed out from this machine, and they fail the same way on main.

Benchmark

2,041 layout nodes (a 40 x 50 grid, each given its first layout in its constructor). Builds of main and this branch alternate, with a fresh process or browser per run. Medians of per-run totals in ms; the last column counts the pairs in which this branch was faster.

main this PR change faster
Chrome: mount 82.3 22.6 -72.5% 15/15
Chrome: first frame 26.9 27.3 +1.5% 8/15
Chrome: next 120 frames 40.5 39.4 -2.7% 7/15
Node: mount 96.8 24.8 -74.4% 21/21
Node: first update 12.5 10.9 -12.9% 15/21
Node: next 100 updates 25.2 28.6 +13.4% 3/21
Node: 100 more after a GC 22.3 24.0 +7.6% 5/21

Chrome 154.0.8037.99 headless with a real Application and Sprite cells; Node 24.16.0 (V8 13.6) without a renderer.

Scripts and raw output:
https://gist.github.com/rynwhite/23a60b891eed47bdf9f0771b67800473

The Node update rows are a renderer-less micro-loop running identical code in both builds; the small gap there does not show up in Chrome frames.

Where the mount cost comes from: redefining an accessor on a prototype invalidates V8's cached property lookups for every object that inherits it. About a third of the extra mount time is inside the defineProperty calls themselves, and redefining an unrelated accessor on Container.prototype once per node reproduces main's cost almost exactly, so the repeated redefinition is the cause.

For context, the same change applied as a patch on 3.2.1 lowered mount CPU by about 15% (median -148 ms, lower in 15 of 15 paired runs) on our own stress screen (about 7,400 Pixi nodes, 2,000 with layouts, larger than a real editor), measured with a different harness.

Risk

Behaviour change: once any container has had a layout, Container.prototype keeps the layout-aware visible setter, even after every layout is removed. On main the override is already in place except between a removal and the next first layout. For a container without a layout, the setter adds one this.layout read to Pixi's setter.

Prepared by Claude Opus 5.5 (max effort) for @rynwhite.

🤖 Generated with Claude Code

The layout setter redefined `visible` on Container.prototype
on every container's first layout, and restored Pixi's
original descriptor whenever any container's layout was
removed. The restore also stripped visibility tracking from
every other container that still had a layout.

Install the override once and never restore it. Its setter
does no layout work for containers without a layout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rynwhite
rynwhite requested a review from Zyie as a code owner October 14, 2026 07:40

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant