Skip to content

Commit 7cec04f

Browse files
committed
Add streaming to readme
1 parent e5dbe50 commit 7cec04f

1 file changed

Lines changed: 100 additions & 0 deletions

File tree

README.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,106 @@ main().catch((error) => {
144144

145145
---
146146

147+
### Streaming
148+
149+
> [!NOTE]
150+
> This is an early version of our streaming implementation.
151+
152+
Preact supports streaming HTML to the client incrementally, flushing `<Suspense>` 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.
153+
154+
Suspended subtrees are rendered into a hidden `<div>` and swapped into place on the client via a small inline script. When rendering a full HTML document (component tree rooted at `<html>`), a `<!DOCTYPE html>` declaration is prepended automatically and deferred content is injected before the closing `</body>` tag to keep the markup valid.
155+
156+
#### `renderToReadableStream` — Web Streams (Edge / Deno / Bun / Cloudflare Workers)
157+
158+
```jsx
159+
import { renderToReadableStream } from 'preact-render-to-string/stream';
160+
import { Suspense, lazy } from 'preact/compat';
161+
162+
const Profile = lazy(() => import('./Profile'));
163+
164+
const App = () => (
165+
<html>
166+
<head><title>My App</title></head>
167+
<body>
168+
<Suspense fallback={<p>Loading profile…</p>}>
169+
<Profile />
170+
</Suspense>
171+
</body>
172+
</html>
173+
);
174+
175+
// Works in any Web Streams environment (Deno, Bun, Cloudflare Workers, …)
176+
export default {
177+
fetch() {
178+
const stream = renderToReadableStream(<App />);
179+
180+
// stream.allReady resolves once all suspended content has been flushed
181+
return new Response(stream, {
182+
headers: { 'Content-Type': 'text/html' }
183+
});
184+
}
185+
};
186+
```
187+
188+
The returned `ReadableStream` has an extra `allReady: Promise<void>` 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).
189+
190+
```js
191+
const stream = renderToReadableStream(<App />);
192+
await stream.allReady; // wait for full render
193+
```
194+
195+
#### `renderToPipeableStream` — Node.js Streams
196+
197+
```jsx
198+
import { createServer } from 'node:http';
199+
import { renderToPipeableStream } from 'preact-render-to-string/stream-node';
200+
import { Suspense, lazy } from 'preact/compat';
201+
202+
const Profile = lazy(() => import('./Profile'));
203+
204+
const App = () => (
205+
<html>
206+
<head><title>My App</title></head>
207+
<body>
208+
<Suspense fallback={<p>Loading profile…</p>}>
209+
<Profile />
210+
</Suspense>
211+
</body>
212+
</html>
213+
);
214+
215+
createServer((req, res) => {
216+
res.setHeader('Content-Type', 'text/html');
217+
218+
const { pipe, abort } = renderToPipeableStream(<App />, {
219+
onShellReady() {
220+
// Called once the synchronous shell is ready to stream.
221+
pipe(res);
222+
},
223+
onAllReady() {
224+
// Called once every suspended subtree has been flushed.
225+
},
226+
onError(error) {
227+
console.error(error);
228+
res.statusCode = 500;
229+
}
230+
});
231+
232+
// Optional: abort the render after a timeout
233+
setTimeout(abort, 10_000);
234+
}).listen(8080);
235+
```
236+
237+
| Option | Description |
238+
|---|---|
239+
| `onShellReady()` | Called synchronously once the initial shell has been rendered and streaming is about to start. Pipe here for fastest TTFB. |
240+
| `onAllReady()` | Called after all `<Suspense>` boundaries have resolved and the stream is complete. |
241+
| `onError(error)` | Called for render errors inside suspended subtrees. |
242+
243+
Calling `abort()` stops the render and destroys the stream; any pending suspended subtrees are dropped.
244+
245+
---
246+
147247
### License
148248

149249
[MIT](http://choosealicense.com/licenses/mit/)

0 commit comments

Comments
 (0)