Open edqwerty1 opened 1 month ago
I think you can get the same effect as your click hack by using:
flushSync(() => {
root.unstable_scheduleHydration(document.getElementById("body"));
});
This doesn't help you in Next.js, since it doesn't expose a way to use unstable_scheduleHydration
... but it probably should?
React does not provide an API to set the priority on a component or suspense boundary for hydration. This means you cannot optimise your application to hydrate part of the app you know users will want to interact with first.
React will do it for you. If a user clicks into a boundary, React will prioritize hydration of the nearest Suspense boundary.
Summary
React does not provide an API to set the priority on a component or suspense boundary for hydration. This means you cannot optimise your application to hydrate part of the app you know users will want to interact with first. Or in my case a part of the application I need to start rendering client side asap to replace a SSR skeleton with the correct personalised content that can only be rendered client side.
The issue is compounded by the way React 18 yields the main thread during hydration to other higher priority events, which while a great idea in theory to improve FID and INP, in practice means 3rd party loaded (gtm) scripts delay the initial hydrating. It would be great if this logic could differentiate between user interactions and scripts added to the call stack?
Detailed here https://github.com/reactwg/react-18/discussions/38#discussioncomment-837161
My real-world use case is an ecommerce application where marketing teams are loading via gtm vast amounts of 3rd party scripts, think tiktok, instagram, bing, and masses of gtm containers. While the ideal solution would be to either trim these down or move them to the worker threads via something like partytown (https://partytown.builder.io/), neither is realistic. As such we need a way to optimise around them.
We can see, on slower windows machines and older mobiles, initial hydration of components take more than 5-8 seconds to start. This provides a negative experience to customers where skeletons are visible, and a worse UX than the legacy MVC sites.
Work arounds
As detailed here there are potential workarounds that already exist
https://github.com/reactwg/react-18/discussions/130
Firstly the
unstable_scheduleHydration
API, however this has since been moved to the hydrateRoot function and is no longer accessible in Next.js (I believe?) (https://github.com/facebook/react/pull/22455/files)The second, and this is where you have to forgive me, is to creatively interpret the following
` Discrete events (eg. click/keypresses) trigger synchronous (selective) hydration in the capture phase if the code in its encapsulating Suspense boundary is ready.
If the event can't be synchronously hydrated then we'll increase the priority of that boundary so it hydrates first when it's ready.`
Which leads to this being an incredible performance optimisation
Because React batches hydration together at Suspense layers, it does an initial render down to the Suspense boundary, then processes the useLayoutEffects, then useEffects, then finally starts the child boundary. This logic when applied in the parent and clicking an element in the first child boundary, forces React into a synchronous render, no longer yielding, and prioritising above all other suspense boundaries the one you clicked.
Evidence
Reproducible example here https://github.com/edqwerty1/hydrate
Visit http://localhost:3000/slow for no click logic, and http://localhost:3000/fast for the improved version.
In this scenario I really need Content Right to hydrate first as I must replace a SSR skeleton with my personalised content
Before
It is delayed by around 5 seconds, these timings are realistic to real world data. The gtm scripts don’t normally block for an entire second, but there are normally a lot more of them.
After
Content Right hydrated in 2.5s, half the time. And more importantly the time between App starting and my component rendering has reduced from 2.3s to 200ms! A very large saving and if I reorder the initial script tags I could in theory delay the 3rd party code until my entire app is ready.
Now this of course isn't idea, by hacking the hydration logic I am potentially causing higher INP scores for real events, and there may be other issues.