-
Notifications
You must be signed in to change notification settings - Fork 47.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
More permissive rehydration logic #10338
Comments
Have you tried the 16 beta yet? I don’t think it has either of these restrictions. |
Interesting! I haven't yet, but I'll give it a try in the morning, maybe it's a bit more malleable for my use case then. Was that a deliberate change or just a consequence of the Fiber changes? |
It was deliberate because we wanted to have streaming which is incompatible with checksum strategy. There’s also likely going to be an explicit hydration API: #10189. |
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contribution. |
Closing this issue after a prolonged period of inactivity. If this issue is still present in the latest release, please create a new issue with up-to-date information. Thank you! |
So for the last little while I've been pursuing an idea called React Snapshot, where instead of running your code in a Node environment to generate static HTML, you run it in a virtual browser (jsdom or chrome headless) and take a snapshot of the DOM at a particularly moment in time, then host the snapshots like any other static file (technique also known as pre-rendering).
I've been tossing around different API choices (geelen/react-snapshot#30) in order to handle components that have async data fetching requirements, but I'm already starting to see real promise in this approach. Because the snapshot environment is so similar to the client one, far fewer changes are needed to get the performance & accessibility benefits of serving real HTML to your users. This is an example of the React Snapshot async API to make a component snapshottable:
The idea is that any async parts of your app can be wrapped in a
snapshot
call, which caches responses and rehydrates on the client. However, I've hit a few walls that I think means I'd need changes to React itself to take this to its logical conclusion. Hence, I wanted to start the discussion about whether such changes would be compatible with React's future direction.Rehydration
As far as I can tell from my experimentation and from reading the code, the two criteria for reusing the existing DOM elements in a pre-rendered HTML page is:
data-react-checksum
present on therootElement
._domID
of each instance in the render tree needs to match thedata-react-id
on each DOM elementBetween those two criteria, its enforced that the structure and the content of the DOM is the same. I can kinda see why both are needed—the checksum is the cheapest way to confirm the structure will be the same, but the ID of each element is needed to actually wire everything up. Also,
data-react-checksum
is just an attribute, and could be calculated off something that's no longer present in the HTML.However, generating the exact right checksum in any other way than the existing SSR API turns out to be pretty difficult!
HTML-escaping woes
I hit this problem where I was rendering the React app like normal, then taking the
innerHTML
of the root container, then passing it toaddChecksumToMarkup
, and not getting the same checksum asReactDOMServer.renderToString
. I first realised I needed to add thedata-reactid
to each element along the way, which wasn't too hard, but still it wasn't working. I figured out it's due toescapeTextContentForBrowser
converting things like'
to'
and"
to"
, meaning that while the content appears the same once rendered, the precise string is not, therefore the checksum is not, and no rehydration takes place.From what I can understand, again by reading the code, React always sanitises the HTML content before generating markup (on server or in client), it's just the fact that once its injected into the DOM,
innerHTML
doesn't re-sanitise things like quotes. They don't technically need to be, as discussed in issue #3879, and so if that were to be changed this particular problem would disappear, but there may well be more I just haven't hit yet. To me, the real issue is needing to have the content be byte-for-byte equivalent, rather than just functionally (and structurally) equivalent.My interim solution
At the moment, I've realised its easier to boot up the app in its entirety, wait for all async processes to take place, then effectively reboot the app using
ReactDOMServer.renderToString
and splice the markup in place. Any side-effects relying oncomponentDidMount
(like CSS injection or meta tags in the HEAD) that affect the DOM outside the React app are preserved, but the markup and checksum of the React-rendered HTML are guaranteed to be correct. It works, but its not ideal. You still have to understand that your components are running in two different "modes", they'll run different lifecycle methods in each, and only one generates the final snapshot. Which I think adds an unreasonable conceptual burden, much the same way server-rendering does.That's really the problem I see with the status quo and why I started looking into this problem in the first place. If snapshot/server rendering requires too much overhead, most people won't do it, which is exactly where we're at. Create-react-app doesn't include any because none of the options are simple enough with a broad enough applicability. The official React Router docs warn agains combining server-rendering and code-splitting. Server-rendering boilerplates include fairly specific webpack hacks to provide the same environment on server and client, etc.
The result is that most people only ever do client-rendering. They serve a blank page & render everything client-side. Code splitting and service worker caching offer useful advantages but imo it's not enough. Snapshot rendering could be the solution, but only if it can offer big benefits for small changes to application code.
My Dream Solution
Architecturally, what I'd like is for an arbitrary React app to be launched on one browser, executed until ready (async resources complete), snapshotted (serialised to HTML), then resumed on another browser. Those snapshots would be generated then cached at the edge of a CDN during deployment, or periodically depending on how often the content changes.
Practically, I think that would require two changes to React's architecture:
The first is for a weaker check for rehydration—some other fingerprint than a hash of the escaped HTML. Some other method for a snapshot to indicate to React to reuse as much of the existing DOM as possible.
The second would be for only parts of the tree to be rehydrated rather than the whole thing. If a component has some side-effect, say in a
componentDidMount
, then the snapshotted HTML would include the result of that side-effect. But when the app boots on the client side, the render method will generate the initial behaviour. At the moment React would replace what's there with what's just been rendered, but it might be preferable to leave the DOM unchanged on the first render, then wire things up later.I don't know the exact specifics of a solution, nor do I know enough of the internals of React as it is now or as it will become, but I wanted to start the discussion and see if there was any interest from the React team & wider community in this use case and direction. I look forward to hearing your thoughts!
The text was updated successfully, but these errors were encountered: