In react-dom/server, what is the difference between renderToString and renderToStaticMarkup, and when would you reach for the static one?
answer
- one is hydratable, one is not
- invisible bookkeeping in the output
- comment markers around boundaries
- separators between adjacent text nodes
- emails and PDFs never hydrate
basics
~20 srenderToString emits React's hydration bookkeeping — comment markers around Suspense boundaries and separators between adjacent text nodes — so the client can attach to the markup. renderToStaticMarkup omits all of it, producing plain HTML for output React will never hydrate.
solid answer
~40 sBoth are buffered renderers in `react-dom/server`: each returns one complete HTML string synchronously, and neither waits for data. The difference is the extra markup in that string. `renderToString` includes what React needs to line the DOM up with the tree on the client — HTML comment markers around Suspense boundaries and `<!-- -->` separators between adjacent text nodes — so the markup can be hydrated. `renderToStaticMarkup` strips that bookkeeping, giving smaller, cleaner HTML that is not meant to be hydrated; hydrating it invites mismatches when two text nodes have merged into one. So use `renderToStaticMarkup` for HTML emails, PDF or preview generation, and pages that ship no React at all, and `renderToString` only when React takes over on the client — and even then a streaming renderer is usually the better pick.
go deeper
Be able to name both functions, say that each returns a finished HTML string, and state the one real difference: only renderToString's output carries the markers React needs to hydrate.
Explain the markers concretely — comment separators between adjacent text nodes, comment markers around Suspense boundaries — and describe the mismatch that follows from hydrating static markup.
Show you would route the decision by consumer: email, PDF and script-free pages get static markup; an application server gets a streaming renderer, not either buffered one. Say why blocking on the full render hurts time-to-first-byte.
Be ready to set the standard: which server-rendering entry points a codebase is allowed to call, how email or document generation stays isolated from the app's rendering path, and how you keep a legacy renderToString call from quietly becoming the default.
## Two buffered renderers `react-dom/server` exposes several ways to turn a React tree into HTML. Two of them are *buffered* (also called blocking): they build the entire HTML string in memory and hand it back to you all at once, as a plain JavaScript string. ```js import { renderToString, renderToStaticMarkup } from 'react-dom/server'; const html = renderToString(<App />); // hydratable const mail = renderToStaticMarkup(<Email />); // not hydratable ``` Neither one streams, and neither one waits for a component that suspends. That part they share. What separates them is how much invisible bookkeeping ends up in the output. ## What hydration needs from the HTML Hydration is the client-side step where React walks the server-produced DOM and attaches its own tree to it instead of building new nodes. To do that reliably it needs a few landmarks in the markup that a human reader would never notice: - **Text separators.** If a component renders `<span>{a}{b}</span>`, the browser sees one merged text node. React inserts an empty comment (`<!-- -->`) between the two so it can tell where one child ends and the next begins. - **Suspense boundary markers.** React writes comment markers around the content of a Suspense boundary so the client knows which region of the DOM corresponds to which boundary. `renderToString` writes those markers. `renderToStaticMarkup` deliberately does not: it produces the HTML a designer would have written by hand. ## Why the static variant exists A large share of server-rendered React output is never hydrated at all. HTML email templates are the classic case: mail clients strip scripts, so there is no React on the receiving end, and stray comment nodes are pure noise (some clients are also famously fussy about markup). The same holds for generating a static marketing page, a printable invoice fed to a PDF renderer, or an HTML snippet embedded in another system's template. For those, `renderToStaticMarkup` gives you a smaller payload and markup that survives downstream processing. ## The failure mode of mixing them up If you pass `renderToStaticMarkup` output to `hydrateRoot`, React has to guess at boundaries it can no longer see. Adjacent text that merged into a single DOM node no longer matches the two children React expects, so you get a hydration mismatch — React logs an error and may discard and re-create that subtree, which is exactly the wasted work server rendering was supposed to avoid. Treat the choice as a one-way door: pick the renderer by whether React will ever run on that HTML. The reverse mistake is milder but still real: using `renderToString` for an email leaves comment nodes in markup that gains nothing from them. ## Where both of them sit today Both are synchronous and blocking, which is their real limitation for an application. The server cannot send a single byte until the last component has rendered, and if part of the tree suspends on data, these renderers do not wait for it — the boundary is emitted with its fallback and the data has to be fetched again on the client. That is why the streaming renderers exist for real apps, and why `renderToString` in an application server is usually a legacy choice rather than a deliberate one. That critique does not apply to `renderToStaticMarkup`'s home turf. An email template has no client, no streaming consumer, and typically no suspending data source: a synchronous string is exactly the right shape, and the API is a good fit rather than a compromise. ## Answering it in an interview State the shared property first (both buffered, both return a string, neither waits for data), then the single distinguishing fact (hydration markers present or absent), then one concrete use case for each. Candidates who describe the difference as "one is faster" or "one strips attributes" are guessing; the difference is about who consumes the HTML, not about speed.
- Why does React insert an empty HTML comment between two adjacent text children in server-rendered markup?Because the browser merges adjacent text into a single DOM text node. React renders `{a}{b}` as two children, so without a separator it cannot tell where the first value ends and the second begins during hydration. The `<!-- -->` comment keeps the two nodes distinct so the client-side tree lines up with the DOM.
- If neither renderer waits for suspended data, what actually ends up in the HTML for a Suspense boundary whose child is still loading?The boundary's fallback. Both buffered renderers emit the fallback markup rather than blocking on the pending resource, and the real content is produced only after the client takes over and the data resolves. That is the core reason a streaming renderer is preferred for any page whose content depends on server data.
- Is renderToString still a reasonable choice for a modern React application server?Rarely. It cannot stream, so time-to-first-byte is bound to the slowest part of the render, and it cannot deliver data-dependent content that suspends. It remains fine for small, synchronous fragments and for tests or tooling that just need a string, but a new application server should use a streaming renderer.
saying these in an interview costs you the question
- Says renderToStaticMarkup is simply a faster renderToString
- Claims static markup can be safely passed to hydrateRoot
- Thinks either renderer waits for suspended data to resolve
- Believes renderToStaticMarkup strips className or style attributes
- Assumes both send HTML to the browser incrementally