I admit I went pretty far in my web development career without understanding that Streamed HTML is a thing. And while I’m admitting things, I’m still not 100% sure when it’s an ideal solution and how best to take advantage of it. But knowing is half the battle sometimes, so let me get into some research and recent writings about it.
Quick summary: Streamed HTML is as you imagine it. Rather than serving the entire HTML document at once, servers serve pieces of it. The browser gets these pieces and can start working on them, even rendering them, so the page can appear to load more quickly. It’s similar to how a progressive JPG loads or how video tends to “stream” as it plays on the web. While browsers can handle it, the rather large caveat is that not all other languages and frameworks are built to handle it.
The first I heard of it was in Taylor Hunt’s The weirdly obscure art of Streamed HTML. At a time, he was doing work for Kroger grocery stores. Every website should have a goal of being fast, but in this case there was a really direct connection that could be drawn. Kroger literally sells mobile phones, and the bestselling phone it sold was the Hot Pepper’s Poblano VLE5 ($15 on sale) and it was a reasonable assumption that “slow 3G” was what users of that phone would experience.
With Streamed HTML in place, you can see the difference:
Not all sites have my API bottlenecking issue, but many have its cousins: database queries and reading files. Showing pieces of a page as data sources finish is useful for almost any dynamic site. For example…
- Showing the header before potentially-slow main content
- Showing main content before sidebars, related posts, comments, and other non-critical information
- Streaming paginated or batched queries as they progress instead of big expensive database queries
Taylor goes on to explain other benefits, like that these chunks of HTML that arrive can be interactive immediately, an import aspect of web performance (as typified by FID or “First Input Delay” metrics, or TTI “Time To Interactive”).
Taylor found that React couldn’t help with this, wanted to use Svelte instead, but found it couldn’t support Streaming HTML. Instead, he landed on Marko. (I highly suggest reading Taylor’s article for the investigations of back and front-end technologies that largely put Streaming HTML in the back seat.)
I’d bet you can imagine why streamed HTML and JavaScript frameworks have a hard time getting along. If the framework code and your usage of that framework loads, but the bits of DOM its looking for aren’t there, well, that ain’t gonna work. Like ReactDOM.render()
needs what it needs — if it can’t find the DOM element to bind to it’s just going to fail, not sit around waiting for it to potentially appear later.
This all came up in mind again as Chris Haynes blogged Streaming HTML out of order without JavaScript. The video captured quite a few people’s attention, me included:
This particular trick was done not just with streamed HTML but also incorporated Web Components and and the unique ability that <slot />
has. Meaning code that arrives in predictable HTML order might get, ahem, slotted into wherever it appears in a Shadow DOM, which may be an entirely different order.
Chris nicely puts a point on what I was trying to say earlier: you need a combination of server and client technology that supports all this to pull it off. Starting with the server. Chris is more optimistic than Taylor was:
You’re in luck here, there is pretty much universal support for this across all languages. I’ve opted for Hono as it’s a lightweight server, built on web standards, that runs on node as well as a wide variety of edge platforms.
Using Node seems to be smart here (see the code), as Node’s support seems to be the most first-class-citizen-y of all the back-end languages. Deno, a spiritual successor to Node, supports it as well, and I tried running their example and it worked great.
Then you might want something client-side to help, to make things a bit more ergonomic to work with:
In the JavaScript world, there aren’t a lot of standalone templating languages that support streaming, but a recent project called SWTL does.
It’s notable that this “Service Worker Template Language” is used here, as Service Workers are aligned with streaming. Service Workers can help do things like intercept requests to the network when offline to return cached data. That can be great for speed and access, which is what streaming HTML is also trying to help with.
This all kinda has the vibes of an old technology coming roaring back because it was a smart thing to do all along, like server-side rendering broadly. For instance, React 18’s renderToPipeableStream
seems to support their server-side efforts more seriously. Solid also supports streaming. It’s not without some downsides, though. Aside from the trickiness of pairing technologies that support it Eric Goldstein notes:
Streaming has a few downsides. Once a response begins and a response code is chosen (e.g. 200), there is no way to change it, so an error occurring during a data fetch will have to let the user know another way.
I wanted to try this myself, but not bother with any of the exotic JavaScript stuff, templating stuff, or out-of-order stuff. I just wanted to see HTML stream at all, from my own servers. Most of my easy-access experimental sites run on PHP, so I tried that. Taylor had noted PHP “requires calling inscrutable output-buffering functions in a finicky order.” Best I could tell, that means to turn off output-buffering so that PHP isn’t holding on to output and instead returning it.
I couldn’t get it to work myself (demo attempt), which is essentially exactly what I was worried about. This stuff, despite being “old” isn’t particularly well documented. And since it seems a little against-the-grain right now, it’s hard to know why it doesn’t work.
My PHP Attempt
<?php
// Set the content type to HTML
header('Content-Type: text/html; charset=UTF-8');
// Turn off output buffering
ob_end_flush();
ob_implicit_flush(true);
// Start the page output
echo '<!DOCTYPE html>';
echo '<html>';
echo '<head><title>Streaming Example</title></head>';
echo '<body>';
echo '<h1>Content is streaming...</h1>';
// Simulate a process that takes time, e.g., database queries or complex calculations
for ($i = 0; $i < 10; $i++) {
// Simulate some server-side processing time
sleep(1); // Sleep for 1 second
// Send a (decently large) piece of content to the browser
echo "<div>New content block $i loaded.</div><details>...</details>";
}
echo '</body>';
echo '</html>';
?>
Code language: PHP (php)
So what did I do wrong? Did I screw up the PHP? That’s very plausible since I’ve literally never even tried this before and the PHP docs didn’t inspire confidence. Does the PHP host this is on (Flywheel — quite WordPress focused which as far as I know doesn’t do streaming HTML) not allow this somehow at some lower level? Is there some kind of caching in front of the PHP (NGINX reverse cache?) that is preventing this? Is the fact that Cloudflare is in front of it all messing it up? I tried bypassing all cache at this URL which seems to have worked, but that doesn’t mean something else Cloudflare-related is happening.
Anyway! If y’all have played with this or are using it and have thoughts, I’d love to hear about it.
Yeah, streaming HTML is weirdly off-by-default in a lot of server/proxy/CDN/etc. settings. I wrote a list of places to check in the Marko docs while I worked for them: https://markojs.com/docs/troubleshooting-streaming/
If I had to guess, it seems like your CDN is trying to be clever — when I refresh your demo page, it then loads instantly with HTTP headers calling it a cache hit, even when my DevTools request no caching. For Kroger Lite, our Akamai contact had to turn off all their value-add “acceleration” features (the irony was not lost on him).
As far as errors during a stream, that’s one of the few HTTP semantics that you CAN still transmit midstream. This discussion is a bit complex as it gets into the underlying plumbing across HTTP versions, but the gist is you want to write garbage to the stream or end it without the right terminator, so caches/search engines/etc. know the stream had an error: https://github.com/marko-js/community/pull/1
As for PHP, check if output buffering is disabled in Apache httpd.conf and chunked transfer-encoding is enabled. Use your local development web server like XAMPP and request web page with curl first.
Have a look at my PHP and Node.js implementation of Pure HTML OOO Streaming (PHOOOS) with an online demo: https://github.com/niutech/phooos