Skip to content

fix(native): one resolved-style cache key per distinct input set - #1

Draft
YevheniiKotyrlo wants to merge 1 commit into
mainfrom
fix/mapping-config-identity
Draft

YevheniiKotyrlo wants to merge 1 commit into
mainfrom
fix/mapping-config-identity

Conversation

@YevheniiKotyrlo

@YevheniiKotyrlo YevheniiKotyrlo commented Aug 20, 2026

Copy link
Copy Markdown
Owner

Base layer of a stack. The submission is upstream: nativewind#434 — review and comment there.

This PR exists so the layer above it (#2) shows only its own six commits instead of seven. Nothing is merged in this fork.

`generateStateHash` keys the resolved-style cache on weak-key identity, so it
shares only when equal inputs arrive as one object. Three defects break that.

**The config is minted per instance.** `styled()` derives it once at module scope;
`useCssElement` derives one per instance from a module constant — same value, fresh
identity, so N identical elements each hold their own entry, sorted rule array and
observable. `getRuleVariation`'s rule clone is keyed on that identity too, so it was
per element rather than per mapping. Deriving through `weakFamily` keys the config
on the mapping. A mapping that is not an object is refused by name rather than
through the `Invalid value used as weak map key` a primitive would otherwise raise
from inside `reactivity`; the predicate is narrower than that WeakMap contract,
since a function is a valid weak key and still not a mapping.

**`family` and `weakFamily` cache on truthiness.** A factory returning `0` is re-run
on every lookup, and `hashKeyFamily` hands out `hashKeyCount++` — so the first weak
key hashed never settles. Of twelve call sites it is the only one whose result can
be falsy, so caching on presence changes nothing else.

**The key is lossy.** Folding N numbers into one and printing it base-36 is a digest,
and `family` returns the entry it holds while ignoring the rules the second caller
brought — so a collision renders one element's styles on another. Over one config
plus one rule, the commonest shape, the fold collides 31.99% of the time. Sorting
and joining is exact and keeps order-independence. The numbers are collected into a
`Float64Array` so the sort is native: ordering them with `Array.prototype.sort` needs
a comparator, and that callback is most of what ordering costs on Hermes — 1.10x the
fold at the median key count against 1.64x. Float64 rather than Int32 because the key
counter is unbounded and an int32 wraps silently at 2^31. Two inherited lines go with the
fold: `generateStateHash`'s empty-string sentinel — unreachable, and collidable once
the key is a join — together with the two parameters no caller passes, and the
loop's `if (!key) continue`, which erased a member from a value whose whole point is
that it erases nothing.

`family` gains `size()` so the invariant is assertable. It reaches `rootVariables`
and `universalVariables`, which are `family` instances published through
`./native-internal` — additive, beside the `delete` and `clear` already on them.

50 identical elements: 50 entries before, 1 after. A 150-control screen on a
physical Android device: 1785 → 270-271. Re-entering a screen grew the cache without
limit (2, 4, 6 …) and is now flat; entries are still never released on unmount,
which is a separate defect.

19 tests across five files, each red first.
@YevheniiKotyrlo YevheniiKotyrlo changed the title fix/mapping config identity fix(native): one resolved-style cache key per distinct input set Aug 20, 2026
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