When using a css-in-js library like emotion of framework like theme-ui (which uses emotion) and with enough components, preact hydration takes longer than react. This increases the first input delay metric for the site when there are a lot of styled components.
My theory is due to the amount of contexts in the app as emotion wraps each styled component in a context so that it has access to the theme. (When removing the contexts hydration times are comparable)
https://github.com/kenny-f/preact-hydration-repro
npm run build
npm run start:prod
Go to localhost:3000
To run the app in `react
Run the performance profiler in chrome devtools (Start profiling and reload page)
Here are some results:
Preact:

you can see from the gif below that the process is extremely deep:

React:

I'm not sure if this a known issue as I couldn't find any information on it. Just our own observations when profiling our app that does not have much functionality right now.
Happy to provide more information if required.
hmm - something is up here. If I extract this code out of Razzle and don't use theme-ui, hydration takes 17ms. I haven't been able to track down whether this is an issue with theme-ui or razzle - my hunch is that something is pulling in react somehow, which could break hook invocation.
This could also just be a stack depth issue. Emotion's Babel plugin combined with theme-ui is honestly a massive amount of work to be doing on every single created VNode - this demo takes half a second to hydrate in both libraries, and the bulk of the time is spent serializing styles and generating style hashes.
_so I don't lose it, here's the isolated hydrate call on codesandbox_
@developit thanks for the response. Based on your reply I've done some more experiments.
First is dropping theme-ui and only use emotion directly:
https://github.com/kenny-f/preact-hydration-repro/tree/emotion-only
preact:

react:

as you can see the timings for both have dropped significantly.
The second is dropping both theme-ui and emotion and manually creating a ThemeContext and wrapping each div in a Consumer
https://github.com/kenny-f/preact-hydration-repro/tree/manual-context
preact:

react:

The timings drop even further here. This does seem to suggest that your theory is correct in that theme-ui and emotion maybe the culprits
Thanks for the extra data. My take-away from yesterday's investigation is twofold:
preact/compat is a potential area of investment, and might be currently exacerbated by a few cases where we currently re-execute vnode hooks during diffing. This can be eliminated by moving to a persistent backing tree like we've been exploring. I'm the short term, it should be possible to detect already-normalized VNodes in compat using vnode._original, and skip re-normalizing them.there is some trick with react scheduler to do calculateChangedBits = () => 0, which stops context propagation similar to shouldComponentUpdate(){return false}. That seems like a likely candidate for the shorter traces? https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberBeginWork.new.js#L2800
could be, yup. certainly for updates, though I would hope that during hydration there are no actual updates occurring?
My guess is that bc preact diffs inside out its just hitting a lot of props.context on the way up so it has more to diff, regardless of updates. Just a shot in the dark. However, id say if this really matters, which im pretty sure it doesnt, just dont use a lib that goes batshit on context wrappers and instead just use css vars, ggez
My guess is that bc preact diffs inside out its just hitting a lot of props.context on the way up so it has more to diff, regardless of updates. Just a shot in the dark. However, id say if this really matters, which im pretty sure it doesnt, just dont use a lib that goes batshit on context wrappers and instead just use css vars, ggez
Appreciate the insight on this and understand that deep stacks might not actually affect hydration performance. However as @developit pointed out, it's about the performance of hydrating Preact compared to React here.
@developit Can we help? (we are actively working on this as a production-live project) Would be happy to contribute back if possible.
My takeaways are:
preact/compat optimisation we could look into(Not related to this issue)
@annez if you need a quick win, replace styled with a stylesheet/rule approach. This way, u will only have 1 context provider.
Alternatively, which is my preferrnce, you can print the theme(s) ahead of time and use a hard coded object, so you can still use theme ui or whatever you want. You can even replace theme colors with css vars so you have a fully dynamic theme . The only downside to this approach is that its harder to let consumers of your library override stuff, but if youre making your own bespoke theme, that shouldnt be an issue for your use case
@developit i should have thought about this sooner, but the material ui repo has a benchmark setup for ssr, which could be useful to test this. I added to their setup to test several implementations of ‘styled’ libraries, and i was supprised to see styled-jss (the standalone version using jss v9) was by far the fastest. 2x faster than emotion and 5x faster than muis version. Emotion was ~50k op/s whereas styled-jss was close to 100k. Most others, including mui, were around 20k. I think this is likely due to using theme context, whereas styled jss only optionally uses the brcast based wrapper and doesnt wrap every component
That's probably it. For benchmarks of this order every function call is very noticeable in the final numbers. Wrapping every node with a component is expensive.
Most helpful comment
@developit i should have thought about this sooner, but the material ui repo has a benchmark setup for ssr, which could be useful to test this. I added to their setup to test several implementations of ‘styled’ libraries, and i was supprised to see styled-jss (the standalone version using jss v9) was by far the fastest. 2x faster than emotion and 5x faster than muis version. Emotion was ~50k op/s whereas styled-jss was close to 100k. Most others, including mui, were around 20k. I think this is likely due to using theme context, whereas styled jss only optionally uses the brcast based wrapper and doesnt wrap every component