Repository navigation
Conversation
generateStateHash keys the resolved-style cache on weak-key identity, so it shares only when equal inputs arrive as one object, and two defects broke that on main. The config was minted per instance: useCssElement derived one from a module constant on every mount, so N identical elements each held their own cache entry, sorted rule array and observable. It is now derived once per mapping through weakFamily, and a mapping that is not an object is refused by name rather than failing inside the WeakMap. The key was lossy: folding the key numbers into one and printing it base-36 is a digest, and family returns the entry it holds whatever rules the second caller brought, so a collision rendered one element's styles on another. The numbers are now sorted and joined, which is exact and order-independent, in a Float64Array so the sort is native and the unbounded key counter does not wrap. family gains size() so the invariant is assertable.
Replace the sorted-and-joined string key with a fixed-size numeric hash: - Each weak-key id goes through Murmur3's 32-bit finalizer, giving two independent 32-bit values that are each summed across the set. Addition is order-independent, so there is no sort, and the key is one number however many rules an element matches. - The key is 32 + 21 of those bits: a 53-bit integer, exactly representable, used directly as the `stylesFamily` key, so no string is built per lookup. - Weak-key ids start at 1, since mix32(0) is 0 and an id of 0 would add nothing to one half of the hash. Against the original fold-and-base36 hash, over the same inputs: config + every pair of ids 2..2001 (1,999,000 sets) original 26,293 / new 0 config + every triple of ids 2..201 (1,313,400 sets) original 26,648 / new 0 random sets, 1-46 rules, ids to 3M (2,000,000 sets) original 1 / new 0 Unlike the exact join, a 53-bit hash can collide in principle: about n^2 / 2^54 for n live cache entries (about 6e-11 at 1,000). Cost on Hermes 0.12, per lookup (key + Map lookup, rules from a Set), against the sorted-and-joined string key: 1-8 rules, small ids: join 3.21 us, this 3.31 us (noise-level) 45 rules, 7-digit ids: join 85.7 us, this 44.6 us A joined string grows with rule count and id size (up to ~320 characters for a heavily styled element in a long session); this key does not. Tests: - state-hash: the string-format check becomes a safe-integer check, plus a config-membership check and a test hashing ~180k realistic key sets (a config plus every pair from 600 keys, and 46-key sets) with no collisions. - style-cache-collisions: renders a 40x40 grid of distinct class combinations and checks every element renders its own styles. Both collision tests fail with the original hash swapped back in. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
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.
Supersedes #434, carrying its commit (by @YevheniiKotyrlo) and replacing its exact string key with a numeric hash.
Problem
The resolved-style cache is keyed by
generateStateHash, built from the identities of an element's config and matched rules, so elements share an entry exactly when those are the same objects. Two defects onmainbreak that:useCssElementderives a config from a module-level mapping on every mount, so identical elements never share a key: 50 identical elements hold 50 cache entries, each with its own sorted rule array and observable.familyreturns whatever entry it holds for a key, so on a collision an element renders another element's styles (seen in practice: avertical-align: topelement renderedobject-fit: contain).Fixing 1 makes 2 worse: once identical elements share one config, they all land on the same key, so one collision mis-styles every one of them instead of a single element.
Solution
weakFamily. A mapping that is not an object is refused by name, rather than failing inside theWeakMapwith a message that names neitherstyled()nor the argument.familygainssize(), so the one-entry-per-input-set invariant is assertable.Tests
config-identity.test.ts: one mapping gives one config, differing mappings keep different keys, a mapping that is not an object is refused by name, and a mutated or per-call mapping behaves correctly.state-hash.test.ts: distinct key sets never share a hash, order does not matter, a key outsideWeakKeyis refused, the hash is a safe integer, ~180k realistic key sets (a config plus every pair from 600 keys, and 46-key sets) never collide, and the config is always part of the key.style-cache.test.tsx: identical elements share one entry, differing elements keep distinct entries, and elements sharing an entry and its config each see the valuenativeStyleMappingtook.style-cache-collisions.test.tsx: a 40×40 grid of distinct class combinations, all mounted at once, each rendering its own styles.Checked by breaking
rules.tson purpose: swapping the current hash back in failsstate-hashand the grid test; hashing only the first rule fails only the grid test; truncating the hash to 32 bits failsstate-hash.Verification
On macOS with Node 24:
yarn lint,yarn typecheckandyarn test(86 suites, 1356 tests) pass.Merge order
#436 builds on #434's commit, which this PR carries, and merges cleanly on top, so this lands first.