Skip to content

fix: index ancestor caches by native tag with Map/Set - #2

Merged
jpudysz merged 1 commit into
mainfrom
fix/collect-ancestors-tag-map
Oct 21, 2026
Merged

jpudysz merged 1 commit into
mainfrom
fix/collect-ancestors-tag-map

Conversation

@jpudysz

@jpudysz jpudysz commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Problem

collectVisibleAncestors and collectAncestors kept their per-call caches in plain arrays indexed by stateNode.canonical.nativeTag. Native tags only grow: every view ever mounted takes a new one. Each call writes hundreds of scattered, large tag indexes into a fresh array. As tags grow, Hermes allocates far more for those writes than for a Map/Set, so the cost grows with session length.

Both consumers call these after every theme change:

  • Unistyles: collectVisibleAncestors in the ShadowRegistry refresh.
  • Uniwind Pro: collectAncestors in refreshReactNodes.

The garbage shows up as extra GCs on unrelated work, like re-renders.

Before (array) After (Map/Set) Paired
Fresh: JS allocated 525 MB [508-535] 472 MB [454-487] -10.3%, 12/12
Fresh: GC count 148 [143-151] 133 [128-139] -10.5%, 12/12
Fresh: GC time 188 ms [162-250] 177 ms [147-209] -8.4%, 10/12
Long: JS allocated 1431 MB [1324-1467] 475 MB [441-489] -66.6%, 12/12
Long: GC count 222 [201-227] 139 [130-146] -36.9%, 12/12
Long: GC time 381 ms [357-564] 202 ms [155-279] -47.7%, 12/12
Long: GC time during mount + re-render 219 ms 158 ms -30%, 12/12

One collectAncestors call over 900 hosts

Native tag reached Before After
~2k 44 kB, 0.19 ms 62 kB, 0.21 ms
~50k 419 kB 62 kB
~550k 4.0-8.3 MB, 0.69 ms 62 kB, 0.21 ms

Per flip

Before, flip allocation grows every round:

  • Fresh session: 1.67 MB to 2.83 MB (flip to dark), 3.3 MB to 5.7 MB (flip to light).
  • Long session: about 9.9 MB / 19.8 MB.

After, it stays flat at 1.68 MB / 3.35 MB.

Wall-clock

Mount (~25 ms), re-render (~19 ms) and unmount (~4 ms) show no difference beyond noise. A flip window (40-90 ms, setTheme to the next idle callback) is dominated by native work and idle scheduling, so it can't resolve a sub-millisecond JS change. With fresh tags the Map/Set version costs about +0.02 ms per call.

Earlier measurement in Unistyles (900 views, 28 rounds per launch, A/B against v3.5.3): total allocation went from 2181 MB to 1952 MB with this change, and GC time dropped about 8.5% below v3.5.3.

@jpudysz
jpudysz marked this pull request as ready for review October 21, 2026 09:28
@jpudysz
jpudysz merged commit 84a52be into main Oct 21, 2026
7 checks passed
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