SSR Streaming
Send HTML to the browser progressively as coroutines resolve — like React's renderToPipeableStream, but in PHP. Three APIs, same result: the browser paints content incrementally.
Generator yield
Return a \Generator from your handler. Each yield $html flushes immediately. No API changes needed.
stream() callback
Get a $write(string) closure. Headers flush before callback runs. Fine-grained control.
sse()
Server-Sent Events. Get an $emit($data, $event, $id) closure. JS EventSource compatible.
Generator SSR — parallel coroutine fetches
$app->route('/stream/ssr', function() {
$start = microtime(true);
return (function() use ($start) {
// 1. Shell sent to browser immediately
yield '<html><body><h1>Page</h1>';
// 2. Parallel fetch via coroutines
$ch = new Channel(2);
go(fn() => [$ch->push(fetchUsers()), co::sleep(1)]);
go(fn() => [$ch->push(fetchPosts()), co::sleep(2)]);
// 3. Stream each section as it resolves
yield '<div id="users">' . $ch->pop() . '</div>'; // arrives at ~1s
yield '<div id="posts">' . $ch->pop() . '</div>'; // arrives at ~2s
yield '</body></html>';
})();
// Total: ~2s (parallel), not 3s (sequential)
});
Live streaming demos
GET
/stream/ssr — Generator yieldOpens a streaming connection. Watch sections appear one by one (1s, then 2s):
Click Run to start…
SSE
/stream/events — Server-Sent Events10 events, 1 second apart. EventSource reconnects automatically on drop.
Click Connect to start…
SSE — $response->sse()
$app->route('/stream/events', function($response) {
$response->sse(function($emit) {
$emit(json_encode(['status' => 'connected']), 'open');
for ($i = 1; $i <= 10; $i++) {
co::sleep(1);
$emit(json_encode(['tick' => $i, 'time' => date('H:i:s')]), 'tick', (string)$i);
}
$emit(json_encode(['done' => true]), 'done');
});
});
// Browser:
// const es = new EventSource('/stream/events');
// es.addEventListener('tick', e => console.log(JSON.parse(e.data)));