Hashed className key collisions
See original GitHub issueCurrent behavior:
We encountered an issue recently where a single page of a moderately sized blog was rendering a component incorrectly. A quick investigation discovered that the generated class name assigned (css-r1ywwf
) was identical to another, correctly-styled component on the page, and the styles associated with the duplicate class were consistent with the correctly-styled component.
This led to some spelunking into how Emotion generates the hash, which lead me to the @emotion/hash package, which has a nearly unintelligible (bit shifting, hexadecimal literals, and single-character variables oh my!) hashing function I hadn’t heard of, so I did what I get paid to do… I Google’d it, which led me to a write-up by the author of MurmurHash that discusses a flaw that can result in the algorithm having a significantly reduced potential output pool. This leads to increased collisions (“a better than 50% chance of finding a collision in a group of 13115”).
Okay, so under the assumption that this is the source of the class-name collision that we are experiencing, I went to try to pull some more details from the buggy page to see if I can create a reproduction, and that’s when I discovered the registered
and inserted
lists in the Emotion Context, which I presume are handling collisions. And this is also when I notice that the div
that is in my DOM with the incorrect class name is actually being passed the correct className
prop, which is generated by a pretty vanilla css
prop if I use React Devtools to inspect it. So the actual DOM node seems to disagree with React Devtools.
It gets weirder, though. I’m using Gatsby, and I noticed that the issue doesn’t present itself if you navigate to the page from another page on the site, only if you load the page directly. I assumed this would be related to server-side rendering, so I checked the static HTML and the correct class and styles are used, so the incorrect class is being assigned as part the rehydration mount, not during SSR or during client-side render from raw data.
Short summary:
- A single Emotion-generated class name is being applied to multiple components that are not using the same styles
- The correct class name is passed to the React element, but the incorrect value is rendered to the DOM
- The issue appears to only present when rehydrating server-side rendered content
- I’m stumped, but anticipate someone with more knowledge of the Emotion internals can pinpoint the issue pretty quickly.
- 😬🙏
Environment information:
react
version: 16.8.6emotion
version:- @emotion/core@10.0.20
- @emotion/css@10.0.14
- @emotion/hash@0.7.3
Issue Analytics
- State:
- Created 4 years ago
- Reactions:3
- Comments:21 (9 by maintainers)
Top GitHub Comments
re. demo repository, unfortunately I don’t have time at the moment. Hopefully the information provided is more useful than not. It sounds like you’re saying that collision handling won’t be implemented in any case - what next steps do you see if two colliding strings were identified in an algorithm known to have collisions?
re. collision checking, it seems the consequences of a collision (css silently applied incorrectly), would be easily more important than any performance hit. P1: do it right, P2: do it fast. The argument that ‘everyone implements it like that’ doesn’t seem convincing.
re. performance, apart from a map lookup, the collision handling code path should only be incurred if there is actually a collision, which (as you note) should be rare. At first glance it seems the performance hit would be minimal, but perhaps I have misunderstood the problem. Even a warning that a collision did occur would be valuable, regardless of whether it was actually resolved by Emotion. It could then be ignored or fixed (with a custom label) at the user’s discretion. Warning for conflicts could be an optional configuration so there is no performance hit at all, turned on in development and off in production.
re. autoLabel demo - unfortunately not at the moment, but hopefully the information is of some value.
re. Babel - I’m using the Gatsby default config. Is there a specific configuration you think might be at fault here?
We’ve also encountered this problem in a large(ish) codebase. I haven’t been able to reproduce in a small demo repository, but have some details of the implementation, and what we’ve done to fix.
We define a component that looks similar to this:
There’s ~200 variations of
name
that produce different glyphs from a custom font (where characters like ‘a’, ‘b’ etc. render different glyphs) andname
is a human readable form that maps to the font character ingetChar
. It is used like<Glyph name="MeaningfulSymbolName" />
.BaseGlyph
defines a few styles like font and color, but is not especially complicated.The reason that we do it this way (rather than rendering the character directly) is that we previously did not have a font for these glyphs and rendered them as separate svg, so they had React components that matched the same component
<Glyph name="..." />
signature. This method using csscontent
allowed us to switch to an emotion-only font implementation without changing the component API. There are certainly other possible implementations that may not have resulted in a name collision.The result of this implementation is needing ~200 different hashes for styles - each style string maybe one or two hundred characters long and of the same length, but varying by a single character.
We render many thousands of
Glyph
s in a single page, and the result of the issue is that one (or more) of the glyphs are consistently replaced by some other glyph because of the name collision. The easiest way to conceptualize the impact is to imagine we are rendering a long and complex page of mathematical formula, with the occasional symbol substituted for some other arbitrary symbol.The issue was resolved in this instance by supplying a custom label name:
this generated:
css-ppg6yw-Glyph-SymbolName1
&css-c954va-Glyph-SymbolName2
, whereas both were generated ascss-112tzn2
previously. As a side note, maybe related, we do haveautoLabel
enabled, but for about 80% of our components the component name is not appended (as in this example).The following also caused emotion to generate distinct class names:
So just making the css string vary on
name
works in this particular instance, but I’m guessing that this just changes the potential for collision, whereas thelabel
solution removes any possibility.A couple of questions:
label
to every component, composing the values of each component parameter?