# preact-render-to-string: server-side Preact, and the streaming it added

> preact-render-to-string renders JSX and Preact components to an HTML string, and version 6 brings Web Streams and Node streams so a Suspense boundary can flush to the client before the data arrives. It is a small package with a large surface, since the same codebase also ships a JSX pragma and a set of microbenchmarks.

**preactjs/preact-render-to-string** — 📄 Universal rendering for Preact: render JSX and Preact components to HTML.

- Repository: https://github.com/preactjs/preact-render-to-string
- Website: http://npm.im/preact-render-to-string
- Stars: 728 · Forks: 102
- Language: JavaScript
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/preactjs-preact-render-to-string

## One function for a virtual DOM on the server

preact-render-to-string renders JSX and Preact components to an HTML string. It works in Node and in the browser, which the README says makes it useful for universal or isomorphic rendering, and the package description is a concise statement of the whole scope: render JSX and Preact components to HTML.

The basic use is a vnode in and a string out:

```js
import { render } from 'preact-render-to-string';
import { h } from 'preact';
/** @jsx h */

let vdom = <div class="foo">content</div>;

let html = render(vdom);
console.log(html);
// <div class="foo">content</div>
```

The comment about the JSX pragma is the practical detail for anyone not using a bundler with automatic JSX. The h import and the pragma comment are what let a plain .js file contain JSX at all.

The component example shows the two kinds of component the renderer handles. A classical class component extending Component with a render method, and a pure functional component taking props and children:

```js
import { render } from 'preact-render-to-string';
import { h, Component } from 'preact';
/** @jsx h */

// Classical components work
class Fox extends Component {
	render({ name }) {
		return <span class="fox">{name}</span>;
	}
}

// ... and so do pure functional components:
const Box = ({ type, children }) => (
	<div class={`box box-${type}`}>{children}</div>
);
```

So there is nothing exotic about the model. You build a vnode tree the same way you would in the browser, and you get a string back.

The project is MIT licensed, the default branch is main, and the last push was on 2026-10-15 with version 6.8.1 published on 2026-10-14, after 6.7.0 in May 2026 and 6.6.7 in March. The homepage for the package is on npm.

## Wiring it into an HTTP server

The Express example is the one most people will copy, and it is short enough to see the whole shape of the integration.

```js
import express from 'express';
import { h } from 'preact';
import { render } from 'preact-render-to-string';
/** @jsx h */

// silly example component:
const Fox = ({ name }) => (
	<div class="fox">
		<h5>{name}</h5>
		<p>This page is all about {name}.</p>
	</div>
);

// basic HTTP server via express:
const app = express();
app.listen(8080);

// on each request, render and return a component:
app.get('/:fox', (req, res) => {
	let html = render(<Fox name={req.params.fox} />);
	// send it back wrapped up as an HTML5 document:
	res.send(`<!DOCTYPE html><html><body>${html}</body></html>`);
});
```

Two things are worth noticing. The render call is synchronous and happens inside the request handler, so the response is a complete string, and the framework does the rest. And the document wrapper is written by hand, which is correct, because a component renderer produces a fragment rather than a document.

That last point is the general shape of using this package. It renders what you give it, and the surrounding HTML document, the head, the assets and the hydration script are your job. For a simple server-rendered page that is a few lines, and for a full application it is a set of conventions you have to establish yourself.

The package also has a JSX entry point, exposed as preact-render-to-string/jsx, which is the pragma that lets you write JSX without importing h in every file, and the package manifest has matching export conditions for the root, the jsx path, the stream path and the stream-node path.

## Error boundaries are off until you turn them on

There is one option in the main package that is off by default, and it will surprise you if you miss it.

Rendering errors can be caught by Preact through getDerivedStateFromErrors or componentDidCatch, but to enable that in preact-render-to-string you have to set errorBoundaries to true on the preact options object:

```js
import { options } from 'preact';

// Enable error boundaries in `preact-render-to-string`
options.errorBoundaries = true;
```

The reason for the opt-in is that error boundaries are a client-side concept. On the server there is no tree to re-render into, so a caught error has nowhere obvious to go, and the renderer has to decide what to emit instead. Leaving the behaviour off by default keeps the default render path predictable, and a server that wants boundary handling opts in.

This is a good example of the kind of detail that separates a rendering library from a rendering function. The same options object that configures Preact in the browser also carries the switch for the server renderer, and the README documents the requirement in one sentence rather than burying it in a caveat.

The other options in the package are the stream entry points discussed below, and the separate Suspense and lazy support, which comes from preact/compat rather than from this package.

## Suspense and lazy need the async variant

Supporting Suspense and lazy components requires the compatibility layer and an asynchronous render, because the whole point of lazy is that the component is not available yet when the render starts.

The install for that setup is:

```bash
npm install preact preact-render-to-string
```

and the lazy component is created in the ordinary way, with a Suspense boundary around it in the application:

```jsx
import { Suspense, lazy } from 'preact/compat';

// Creation of the lazy component
const HomePage = lazy(() => import('./pages/home'));

const Main = () => {
	return (
		<Suspense fallback={<p>Loading</p>}>
			<HomePage />
		</Suspense>
	);
};
```

The rendering then uses renderToStringAsync and awaits the result:

```jsx
import { renderToStringAsync } from 'preact-render-to-string';
import { Main } from './main';

const main = async () => {
	// Rendering of lazy components
	const html = await renderToStringAsync(<Main />);

	console.log(html);
	// <h1>Home page</h1>
};

// Execution & error handling
main().catch((error) => {
	console.error(error);
});
```

The interesting detail is in the output. The awaited result is the resolved document, with the Home page content in place of the fallback, and the promise is awaited inside a try-and-catch style handler so a failed dynamic import is an error you log rather than an unhandled rejection.

So there are three render functions in the package, and the choice is between speed and shape. render is synchronous and returns a string. renderToStringAsync awaits the suspended content and returns a complete string. And the stream variants return a stream so the client can start parsing before the data is ready.

## Streaming, and the two environments it targets

The streaming implementation is the newest part of the package, and the README marks it as an early version.

The idea is that Preact supports streaming HTML to the client incrementally, flushing Suspense fallbacks immediately and replacing them with the resolved content as data arrives. The stated benefit is a reduction in time to first byte, and that the browser can start parsing earlier.

There are two entry points, one per stream flavour. renderToReadableStream gives a Web Streams ReadableStream and works in any Web Streams environment, so Deno, Bun and Cloudflare Workers:

```jsx
import { renderToReadableStream } from 'preact-render-to-string/stream';
import { Suspense, lazy } from 'preact/compat';

const Profile = lazy(() => import('./Profile'));

const App = () => (
	<html>
		<head><title>My App</title></head>
		<body>
			<Suspense fallback={<p>Loading profile…</p>}>
				<Profile />
			</Suspense>
		</body>
	</html>
);
```

and renderToPipeableStream gives a Node.js stream, with the callback set that a Node server expects:

```jsx
import { createServer } from 'node:http';
import { renderToPipeableStream } from 'preact-render-to-string/stream-node';
import { Suspense, lazy } from 'preact/compat';

const Profile = lazy(() => import('./Profile'));

const App = () => (
	<html>
		<head><title>My App</title></head>
		<body>
			<Suspense fallback={<p>Loading profile…</p>}>
				<Profile />
			</Suspense>
		</body>
	</html>
);

createServer((req, res) => {
	res.setHeader('Content-Type', 'text/html');

	const { pipe, abort } = renderToPipeableStream(<App />, {
		onShellReady() {
			// Called once the synchronous shell is ready to stream.
			pipe(res);
		},
		onAllReady() {
			// Called once every suspended subtree has been flushed.
		},
		onError(error) {
			console.error(error);
			res.statusCode = 500;
		}
	});

	// Optional: abort the render after a timeout
	setTimeout(abort, 10_000);
}).listen(8080);
```

The three callbacks are the whole API. onShellReady fires synchronously once the initial shell has been rendered and streaming is about to start, and the table says to pipe there for the fastest time to first byte. onAllReady fires after all the Suspense boundaries have resolved. onError fires for render errors inside suspended subtrees, and the example sets a 500 status code. Calling abort stops the render and destroys the stream, and any pending suspended subtrees are dropped, which is what the optional setTimeout uses to cap the render at ten seconds.

## The allReady promise, and when to wait for it

One property on the Web Streams version deserves its own attention, because it decides whether your response is complete.

The returned ReadableStream has an extra allReady property, a promise that resolves once every suspended subtree has been written. The README says to await it before sending the response if you need the complete document before anything is flushed, giving the example of a static export, and notes that at that point you might be better off using renderToStringAsync instead:

```js
const stream = renderToReadableStream(<App />);
await stream.allReady; // wait for full render
```

That is a genuinely useful escape hatch, and the advice to switch function is the more useful part. If you want the whole document before the first byte, a stream is the wrong abstraction and the async string render does the same job with less machinery.

The other thing to notice is that the streaming work is recent and the README labels it an early version of the implementation. The package is at 6.8.1 with a 6.x line that has moved through 6.6.7, 6.7.0 and 6.8.1, and the repository layout reflects a mature package with a changesets directory for release notes, a benchmarks directory with separate V8 and benchmarkjs runs, a config directory holding the export fix-up scripts for different Node versions, a demo directory with its own Vite setup, a jsx.js and jsx.d.ts pair for the pragma, and a vitest config for the tests.

## Where preact-render-to-string is the wrong choice

The first wrong case is a framework that already renders for you. Most server frameworks have their own rendering integration, and if yours does, this package is a second way to do the same thing with a second set of conventions. The value here is that it is small, standalone and does one thing, which is exactly what makes it redundant when the framework has already made the choice for you.

The second is a project that is not using Preact. The package is named for Preact and its error boundary behaviour is Preact's, and its Suspense and lazy support comes from preact/compat. If your components are React, the equivalent tool already exists in the React ecosystem, and using a Preact renderer for React trees is not a shortcut.

The third is a workload where streaming does not matter. If your pages are small and your data is fast, a synchronous render is simpler and the streaming path adds a callback set, an abort timer and a decision about allReady. The README itself marks the streaming implementation as early, which is a fair signal about how much maturity to expect.

The fourth is a project that needs the document, not the fragment. This package renders a component tree to a string, and the doctype, the head and the hydration bootstrap are yours to write. A framework that renders a whole page handles that, and writing it by hand is work you only do once but then own forever.

The alternative worth weighing is rendering on the client only, with no server pass at all, which is what a single-page Preact application does. You lose the first-paint HTML and the indexability, and you gain a single code path and no hydration mismatch to reason about, which for an application behind a login is frequently the right trade.

## Conclusion

Adopt preact-render-to-string if you render Preact on the server and want the same components and the same JSX in the browser and in Node, since the package keeps the two environments aligned and now supports streaming responses where a plain string would block. Keep away if your server framework already renders for you, because most of them can produce HTML from a Preact tree without a separate renderer, and the streaming implementation is described as an early version. Before you rely on the streaming path, read the note about it, and note the release cadence with 6.8.1 published on 2026-10-14 after 6.7.0 in May and the last push on 2026-10-15.

## FAQ

### How do I render a Preact component to an HTML string?

Import render from preact-render-to-string, build a vnode with the h function from preact, and pass it to render, which returns the HTML string synchronously. If you use a bundler with automatic JSX you do not need the h import, otherwise you need the /** @jsx h */ pragma comment.

### How do I enable error boundaries when rendering on the server?

Set options.errorBoundaries to true on the preact options object. The README notes that rendering errors can otherwise be caught by Preact through getDerivedStateFromErrors or componentDidCatch, and the feature is opt-in in preact-render-to-string.

### What is the difference between render, renderToStringAsync and the stream functions?

render is synchronous and returns a string. renderToStringAsync awaits the suspended content and returns a complete string, which is what lazy and Suspense components need. renderToReadableStream and renderToPipeableStream return a stream so the client can start parsing before the data arrives.

### When should I await the allReady promise in preact-render-to-string?

Await it when you need the complete document before anything is flushed, for example for a static export. The README also notes that in that case you might be better off using renderToStringAsync instead, since a stream is the wrong abstraction for a full-document response.

## Sources

- [License: MIT](https://github.com/preactjs/preact-render-to-string/blob/main/LICENSE)
- [preactjs/preact-render-to-string on GitHub](https://github.com/preactjs/preact-render-to-string)
- [Project website](http://npm.im/preact-render-to-string)
- [README](https://github.com/preactjs/preact-render-to-string/blob/main/README.md)
- [Releases](https://github.com/preactjs/preact-render-to-string/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/preactjs-preact-render-to-string
