From 811f05bce25de3451f0ab1d8925163ee0ae54635 Mon Sep 17 00:00:00 2001
From: Jovi De Croock
Date: Fri, 20 Feb 2026 07:28:02 +0100
Subject: [PATCH 1/3] Add streaming to readme
---
README.md | 98 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 98 insertions(+)
diff --git a/README.md b/README.md
index 34617c04..47907cca 100644
--- a/README.md
+++ b/README.md
@@ -144,6 +144,104 @@ main().catch((error) => {
---
+### Streaming
+
+> [!NOTE]
+> This is an early version of our streaming implementation.
+
+Preact supports streaming HTML to the client incrementally, flushing `` fallbacks immediately and replacing them with the resolved content as data arrives. This reduces Time to First Byte and allows the browser to start parsing earlier.
+
+#### `renderToReadableStream` — Web Streams (Edge / Deno / Bun / Cloudflare Workers)
+
+```jsx
+import { renderToReadableStream } from 'preact-render-to-string/stream';
+import { Suspense, lazy } from 'preact/compat';
+
+const Profile = lazy(() => import('./Profile'));
+
+const App = () => (
+
+ My App
+
+ Loading profile…
}>
+
+
+
+
+);
+
+// Works in any Web Streams environment (Deno, Bun, Cloudflare Workers, …)
+export default {
+ fetch() {
+ const stream = renderToReadableStream();
+
+ // stream.allReady resolves once all suspended content has been flushed
+ return new Response(stream, {
+ headers: { 'Content-Type': 'text/html' }
+ });
+ }
+};
+```
+
+The returned `ReadableStream` has an extra `allReady: Promise` property that resolves once every suspended subtree has been written. Await it before sending the response if you need the complete document before anything is flushed (e.g. for static export).
+
+```js
+const stream = renderToReadableStream();
+await stream.allReady; // wait for full render
+```
+
+#### `renderToPipeableStream` — Node.js Streams
+
+```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 = () => (
+
+ My App
+
+ Loading profile…}>
+
+
+
+
+);
+
+createServer((req, res) => {
+ res.setHeader('Content-Type', 'text/html');
+
+ const { pipe, abort } = renderToPipeableStream(, {
+ 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);
+```
+
+| Option | Description |
+|---|---|
+| `onShellReady()` | Called synchronously once the initial shell has been rendered and streaming is about to start. Pipe here for fastest TTFB. |
+| `onAllReady()` | Called after all `` boundaries have resolved and the stream is complete. |
+| `onError(error)` | Called for render errors inside suspended subtrees. |
+
+Calling `abort()` stops the render and destroys the stream; any pending suspended subtrees are dropped.
+
+---
+
### License
[MIT](http://choosealicense.com/licenses/mit/)
From 6c0ace60f7e86cfd91666f3cb39bc309d366f4ff Mon Sep 17 00:00:00 2001
From: Jovi De Croock
Date: Fri, 20 Feb 2026 07:33:10 +0100
Subject: [PATCH 2/3] Apply suggestions from code review
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 47907cca..3741c188 100644
--- a/README.md
+++ b/README.md
@@ -183,7 +183,7 @@ export default {
};
```
-The returned `ReadableStream` has an extra `allReady: Promise` property that resolves once every suspended subtree has been written. Await it before sending the response if you need the complete document before anything is flushed (e.g. for static export).
+The returned `ReadableStream` has an extra `allReady: Promise` property that resolves once every suspended subtree has been written. Await it before sending the response if you need the complete document before anything is flushed (e.g. for static export). At which point you might be better off using `renderToStringAsync` though.
```js
const stream = renderToReadableStream();
From 5e1b20b436e5a81575b4022e5e2535716745decb Mon Sep 17 00:00:00 2001
From: Jovi De Croock
Date: Fri, 20 Feb 2026 07:41:34 +0100
Subject: [PATCH 3/3] Apply suggestion from @JoviDeCroock
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 3741c188..d5cb6915 100644
--- a/README.md
+++ b/README.md
@@ -151,7 +151,7 @@ main().catch((error) => {
Preact supports streaming HTML to the client incrementally, flushing `` fallbacks immediately and replacing them with the resolved content as data arrives. This reduces Time to First Byte and allows the browser to start parsing earlier.
-#### `renderToReadableStream` — Web Streams (Edge / Deno / Bun / Cloudflare Workers)
+#### `renderToReadableStream` — Web Streams
```jsx
import { renderToReadableStream } from 'preact-render-to-string/stream';