Open kenny-f opened 4 years ago
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.
Issue
When using a css-in-js library like
emotion
of framework liketheme-ui
(which usesemotion
) 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)Reproduction
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
Steps to reproduce
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.