{"id":1193,"date":"2024-03-11T10:26:44","date_gmt":"2024-03-11T16:26:44","guid":{"rendered":"https:\/\/frontendmasters.com\/blog\/?p=1193"},"modified":"2024-03-11T11:06:56","modified_gmt":"2024-03-11T17:06:56","slug":"streaming-html","status":"publish","type":"post","link":"https:\/\/frontendmasters.com\/blog\/streaming-html\/","title":{"rendered":"Streaming HTML"},"content":{"rendered":"\n<p>I admit I went pretty far in my web development career without understanding that Streamed HTML is a thing. And while I&#8217;m admitting things, I&#8217;m still not 100% sure when it&#8217;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.<\/p>\n\n\n\n<p class=\"learn-more\">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&#8217;s similar to how <a href=\"https:\/\/cloudinary.com\/blog\/progressive_jpegs_and_green_martians\">a progressive JPG loads<\/a> or how <a href=\"https:\/\/www.cloudflare.com\/learning\/video\/what-is-streaming\/\">video tends to &#8220;stream&#8221;<\/a> 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.<\/p>\n\n\n\n<p>The first I heard of it was in Taylor Hunt&#8217;s <a href=\"https:\/\/dev.to\/tigt\/the-weirdly-obscure-art-of-streamed-html-4gc2\">The weirdly obscure art of Streamed HTML<\/a>. 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 <em>bestselling<\/em> phone it sold was the Hot Pepper\u2019s Poblano VLE5 ($15 on sale) and it was a reasonable assumption that &#8220;slow 3G&#8221; was what users of that phone would experience. <\/p>\n\n\n\n<p>With Streamed HTML in place, you can see the difference:<\/p>\n\n\n\n\t\t<figure class=\"wp-block-jetpack-videopress jetpack-videopress-player\" style=\"\" >\n\t\t\t<div class=\"jetpack-videopress-player__wrapper\"> <iframe title=\"VideoPress Video Player\" aria-label='VideoPress Video Player' width='500' height='281' src='https:\/\/videopress.com\/embed\/vjw2IBfA?cover=1&amp;autoPlay=0&amp;controls=1&amp;loop=0&amp;muted=0&amp;persistVolume=1&amp;playsinline=0&amp;preloadContent=metadata&amp;useAverageColor=1&amp;hd=0' frameborder='0' allowfullscreen data-resize-to-parent=\"true\" allow='clipboard-write'><\/iframe><script src='https:\/\/v0.wordpress.com\/js\/next\/videopress-iframe.js?m=1674852142'><\/script><\/div>\n\t\t\t<figcaption>Streamed HTML on the right, where static HTML bits are already in place and products requiring API calls are loaded later. Non-streamed HTML on the left arrives all at once.<\/figcaption>\n\t\t\t\n\t\t<\/figure>\n\t\t\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>Not all sites have my API bottlenecking issue, but many have its cousins: database queries and reading files.&nbsp;<strong>Showing pieces of a page as data sources finish is useful for almost any dynamic site.<\/strong>&nbsp;For example\u2026<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Showing the header before potentially-slow main content<\/li>\n\n\n\n<li>Showing main content before sidebars, related posts, comments, and other non-critical information<\/li>\n\n\n\n<li>Streaming paginated or batched queries as they progress instead of big expensive database queries<\/li>\n<\/ul>\n<\/blockquote>\n\n\n\n<p>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 <a href=\"https:\/\/web.dev\/articles\/optimize-fid#:~:text=First%20Input%20Delay%20(FID)%20is,to%20respond%20to%20that%20interaction.\">&#8220;First Input Delay&#8221;<\/a> metrics, or TTI &#8220;Time To Interactive&#8221;). <\/p>\n\n\n\n<p>Taylor found that<em> <\/em>React couldn&#8217;t help with this, <em>wanted<\/em> to use Svelte instead, but found it couldn&#8217;t support Streaming HTML. Instead, he landed on <a href=\"https:\/\/markojs.com\/\">Marko<\/a>. (I highly suggest reading <a href=\"https:\/\/dev.to\/tigt\/the-weirdly-obscure-art-of-streamed-html-4gc2\">Taylor&#8217;s article<\/a> for the investigations of back and front-end technologies that largely put Streaming HTML in the back seat.)<\/p>\n\n\n\n<p>I&#8217;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&#8217;t there, well, that ain&#8217;t gonna work. Like <code>ReactDOM.render()<\/code> needs what it needs \u2014 if it can&#8217;t find the DOM element to bind to it&#8217;s just going to fail, not sit around waiting for it to potentially appear later.<\/p>\n\n\n\n<p>This all came up in mind again as Chris Haynes blogged <a href=\"https:\/\/lamplightdev.com\/blog\/2024\/01\/10\/streaming-html-out-of-order-without-javascript\/?ck_subscriber_id=2246502080\">Streaming HTML out of order without JavaScript<\/a>. The video captured quite a few people&#8217;s attention, me included:<\/p>\n\n\n\n\t\t<figure class=\"wp-block-jetpack-videopress jetpack-videopress-player\" style=\"\" >\n\t\t\t<div class=\"jetpack-videopress-player__wrapper\"> <iframe title=\"VideoPress Video Player\" aria-label='VideoPress Video Player' width='500' height='359' src='https:\/\/videopress.com\/embed\/4asFr7Oz?cover=1&amp;autoPlay=0&amp;controls=1&amp;loop=0&amp;muted=0&amp;persistVolume=1&amp;playsinline=0&amp;preloadContent=metadata&amp;useAverageColor=1&amp;hd=0' frameborder='0' allowfullscreen data-resize-to-parent=\"true\" allow='clipboard-write'><\/iframe><script src='https:\/\/v0.wordpress.com\/js\/next\/videopress-iframe.js?m=1674852142'><\/script><\/div>\n\t\t\t\n\t\t\t\n\t\t<\/figure>\n\t\t\n\n\n<p>This particular trick was done not <em>just<\/em> with streamed HTML but also incorporated Web Components and and the unique ability that <code>&lt;slot \/&gt;<\/code> 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. <\/p>\n\n\n\n<p>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: <\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>You&#8217;re in luck here, there is pretty much universal support for this across all languages. I&#8217;ve opted for&nbsp;<strong><a href=\"https:\/\/hono.dev\/\">Hono<\/a><\/strong>&nbsp;as it&#8217;s a lightweight server, built on web standards, that runs on node as well as a wide variety of edge platforms.<\/p>\n<\/blockquote>\n\n\n\n<p>Using Node seems to be smart here (<a href=\"https:\/\/github.com\/lamplightdev\/ooo\">see the code<\/a>), as Node&#8217;s support seems to be the most first-class-citizen-y of all the back-end languages. Deno, a spiritual successor to Node, <a href=\"https:\/\/examples.deno.land\/http-server-streaming\">supports it as well<\/a>, and I tried running their example and it worked great.<\/p>\n\n\n\n\t\t<figure class=\"wp-block-jetpack-videopress jetpack-videopress-player\" style=\"\" >\n\t\t\t<div class=\"jetpack-videopress-player__wrapper\"> <iframe title=\"VideoPress Video Player\" aria-label='VideoPress Video Player' width='500' height='303' src='https:\/\/videopress.com\/embed\/HAKthOUN?cover=1&amp;autoPlay=0&amp;controls=1&amp;loop=0&amp;muted=0&amp;persistVolume=1&amp;playsinline=0&amp;preloadContent=metadata&amp;useAverageColor=1&amp;hd=0' frameborder='0' allowfullscreen data-resize-to-parent=\"true\" allow='clipboard-write'><\/iframe><script src='https:\/\/v0.wordpress.com\/js\/next\/videopress-iframe.js?m=1674852142'><\/script><\/div>\n\t\t\t<figcaption>Note that it&#8217;s the document itself that is being updated, not client-side JavaScript doing that updating.<\/figcaption>\n\t\t\t\n\t\t<\/figure>\n\t\t\n\n\n<p>Then you might want something client-side to help, to make things a bit more ergonomic to work with:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>In the JavaScript world, there aren&#8217;t a lot of standalone templating languages that support streaming, but a recent project called&nbsp;<strong><a href=\"https:\/\/github.com\/thepassle\/swtl\">SWTL<\/a><\/strong>&nbsp;does.<\/p>\n<\/blockquote>\n\n\n\n<p>It&#8217;s notable that this &#8220;Service Worker Template Language&#8221; 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.<\/p>\n\n\n\n<p>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&#8217;s <code><a href=\"https:\/\/react.dev\/reference\/react-dom\/server\/renderToPipeableStream\">renderToPipeableStream<\/a><\/code> seems to support their server-side efforts more seriously. Solid also <a href=\"https:\/\/www.solidjs.com\/docs\/latest\/api#rendertostream\">supports streaming<\/a>. It&#8217;s not without some downsides, though. Aside from the trickiness of pairing technologies that support it <a href=\"https:\/\/www.babbel.com\/en\/magazine\/exploring-web-rendering-streaming-html\">Eric Goldstein notes<\/a>:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>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. <\/p>\n<\/blockquote>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p>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 &#8220;requires calling&nbsp;<a href=\"https:\/\/www.php.net\/manual\/en\/book.outcontrol.php\">inscrutable output-buffering functions<\/a>&nbsp;in a finicky order.&#8221; Best I could tell, that means to <em>turn off<\/em> output-buffering so that PHP isn&#8217;t holding on to output and instead returning it. <\/p>\n\n\n\n<p>I couldn&#8217;t get it to work myself (<a href=\"https:\/\/chriscoyier.net\/examples\/streaming\/\">demo attempt<\/a>), which is essentially exactly what I was worried about. This stuff, despite being &#8220;old&#8221; isn&#8217;t particularly well documented. And since it seems a little against-the-grain right now, it&#8217;s hard to know <em>why<\/em> it doesn&#8217;t work. <\/p>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary>My PHP Attempt<\/summary><pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-meta\">&lt;?php<\/span>\n<span class=\"hljs-comment\">\/\/ Set the content type to HTML<\/span>\nheader(<span class=\"hljs-string\">'Content-Type: text\/html; charset=UTF-8'<\/span>);\n\n<span class=\"hljs-comment\">\/\/ Turn off output buffering<\/span>\nob_end_flush();\nob_implicit_flush(<span class=\"hljs-keyword\">true<\/span>);\n\n<span class=\"hljs-comment\">\/\/ Start the page output<\/span>\n<span class=\"hljs-keyword\">echo<\/span> <span class=\"hljs-string\">'&lt;!DOCTYPE html&gt;'<\/span>;\n<span class=\"hljs-keyword\">echo<\/span> <span class=\"hljs-string\">'&lt;html&gt;'<\/span>;\n<span class=\"hljs-keyword\">echo<\/span> <span class=\"hljs-string\">'&lt;head&gt;&lt;title&gt;Streaming Example&lt;\/title&gt;&lt;\/head&gt;'<\/span>;\n<span class=\"hljs-keyword\">echo<\/span> <span class=\"hljs-string\">'&lt;body&gt;'<\/span>;\n<span class=\"hljs-keyword\">echo<\/span> <span class=\"hljs-string\">'&lt;h1&gt;Content is streaming...&lt;\/h1&gt;'<\/span>;\n\n<span class=\"hljs-comment\">\/\/ Simulate a process that takes time, e.g., database queries or complex calculations<\/span>\n<span class=\"hljs-keyword\">for<\/span> ($i = <span class=\"hljs-number\">0<\/span>; $i &lt; <span class=\"hljs-number\">10<\/span>; $i++) {\n  <span class=\"hljs-comment\">\/\/ Simulate some server-side processing time<\/span>\n  sleep(<span class=\"hljs-number\">1<\/span>); <span class=\"hljs-comment\">\/\/ Sleep for 1 second<\/span>\n\t\n  <span class=\"hljs-comment\">\/\/ Send a (decently large) piece of content to the browser<\/span>\n  <span class=\"hljs-keyword\">echo<\/span> <span class=\"hljs-string\">\"&lt;div&gt;New content block $i loaded.&lt;\/div&gt;&lt;details&gt;...&lt;\/details&gt;\"<\/span>;\n}\n\n<span class=\"hljs-keyword\">echo<\/span> <span class=\"hljs-string\">'&lt;\/body&gt;'<\/span>;\n<span class=\"hljs-keyword\">echo<\/span> <span class=\"hljs-string\">'&lt;\/html&gt;'<\/span>;\n<span class=\"hljs-meta\">?&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre><\/details>\n\n\n\n<p>So what did I do wrong? Did I screw up the PHP? That&#8217;s very plausible since I&#8217;ve literally never even tried this before and the PHP docs didn&#8217;t inspire confidence. Does the PHP host this is on (Flywheel \u2014 quite WordPress focused which as far as I know doesn&#8217;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&#8217;t mean something <em>else<\/em> Cloudflare-related is happening. <\/p>\n\n\n\n<p>Anyway! If y&#8217;all have played with this or are using it and have thoughts, I&#8217;d love to hear about it. <\/p>\n","protected":false},"excerpt":{"rendered":"<p>I admit I went pretty far in my web development career without understanding that Streamed HTML is a thing. And while I&#8217;m admitting things, I&#8217;m still not 100% sure when it&#8217;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 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":1209,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"sig_custom_text":"","sig_image_type":"featured-image","sig_custom_image":0,"sig_is_disabled":false,"inline_featured_image":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[1],"tags":[31,127,36],"class_list":["post-1193","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post","tag-html","tag-streaming-html","tag-web-components"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/03\/stream-thumb.jpg?fit=1000%2C500&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/1193","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/comments?post=1193"}],"version-history":[{"count":7,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/1193\/revisions"}],"predecessor-version":[{"id":1210,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/1193\/revisions\/1210"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media\/1209"}],"wp:attachment":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media?parent=1193"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/categories?post=1193"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/tags?post=1193"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}