From the ReactDOMServer docs (emphasis mine):
If you call
ReactDOM.hydrate()on a node that already has this server-rendered markup, React will preserve it and only attach event handlers, allowing you to have a very performant first-load experience.
The text in bold is the main difference. render may change your node if there is a difference between the initial DOM and the current DOM. hydrate will only attach event handlers.
From the Github issue that introduced hydrate as a separate API:
If this is your initial DOM:
<div id="container">
<div class="spinner">Loading...</div>
</div>
and then call:
ReactDOM.render(
<div class="myapp">
<span>App</span>
</div>,
document.getElementById('container')
)
intending to do a client-side only render (not hydration).
Then you end with
<div id="container">
<div class="spinner">
<span>App</span>
</div>
</div>
Because we don’t patch up the attributes.
Just FYI the reason they didn’t patch the attributes is
… This would be really slow to hydrate in the normal hydration mode and slow down initial render into a non-SSR tree.