HTMLRewriter — streaming HTML transforms for SSR + edge
Accept, Phase 2 — not blocking Phase 0/1. Architecturally correct (same API on Bun and Workers). The "unlocks streaming SSR" framing in the summary is overst…
Review verdict (2026-05-24)
Accept, Phase 2 — not blocking Phase 0/1. Architecturally correct (same API on Bun and Workers). The "unlocks streaming SSR" framing in the summary is overstated — renderToReadableStream is already streaming and the shell injection happens around it, not inside it. The real wins are: (1) safer than string concat against minified shells, (2) becomes the prerequisite for [[rfc-bun-csrf]]'s renderer auto-injection of CSRF inputs, (3) cleaner extension point for user-supplied rewriters.
Phase 0/1 spec 03 stays with the documented string-injection approach until this lands.
Summary
HTMLRewriter is Bun's (and Cloudflare's) streaming HTML rewriter. It transforms HTML as a Response body streams through it, without buffering. Patties should use it for the dev HMR script injection, island bootstrap markup, and any user-defined response transforms — replacing today's string-concat injection.
Motivation
03-render currently concatenates <script> tags onto the rendered HTML string before returning a Response. That:
- forces full buffering (defeats React streaming SSR),
- requires careful
</body>matching that breaks on minified shells, - duplicates work for the edge adapter (12).
HTMLRewriter lets us declare "inject this script before </body>" and "set this attribute on <html>" as element handlers and stream the result. It's the same API on Bun and on Cloudflare Workers, so 12-edge gets it for free.
Proposal
- 03-render: replace the string-injection path with an
HTMLRewriterpipeline. The renderer produces a Response fromreact-dom/server.renderToReadableStream, then pipes through one rewriter that adds the HMR script (dev) and island bootstrap. - 06-islands: island hydration script becomes an HTMLRewriter element handler attached to elements with
data-island. - 12-edge-adapters: document that the same rewriter ships unchanged to Cloudflare/Workerd.
Sample:
const rewriter = new HTMLRewriter()
.on("body", { element(el) { el.append(hmrScript, { html: true }); } });
return rewriter.transform(ssrResponse);
Trade-offs
- Adds a streaming transform stage — slightly higher per-request overhead than a string concat, but unlocks streaming SSR.
- Non-Bun, non-CF runtimes (Vercel Node) need a polyfill — adapter problem, not core.
Open questions
- Order of rewriters when users add their own (middleware extension point)?
- Does
HTMLRewriterinteract correctly with React's<Suspense>boundary chunks? Needs a streaming benchmark.