DEV Community

Cover image for The weirdly obscure art of Streamed HTML

The weirdly obscure art of Streamed HTML

Taylor Hunt on March 15, 2022

My goal from last time: reuse our existing APIs in a demo of the fastest possible version of our ecommerce website… and keep it under 20 kilobytes....
Collapse
 
jonrandy profile image
Jon Randy 🎖️ • Edited

Years ago, I remember using a similar technique to stream JS that would update a progress bar for a long running server process. The PHP process would keep sending JS (comment lines I believe) to keep the connection open, and would occasionally drop in an updateProgress() call. The browser was happy to run the JS as it came in. I was amazed it worked, but work it did... Streaming JS!

Collapse
 
tigt profile image
Taylor Hunt

Do you remember if it was called COMET, or related to that?

Collapse
 
jonrandy profile image
Jon Randy 🎖️

I wrote it all myself. Didn't name it

Collapse
 
lexlohr profile image
Alex Lohr

One of the maintainers of Marko has written his own reactive framework in the meantime called Solid.js, which also adopted streaming capabilities.

It is slightly more react-like than Marko, so your other developers might feel more at home with it. Maybe you want to check it out.

Collapse
 
tigt profile image
Taylor Hunt

Yeah, Ryan and I chat frequently in the Marko Discord. If Solid had adopted those streaming capabilities back when I embarked on this demo, it probably would have been a compelling option.

Collapse
 
yw662 profile image
yw662

I just noticed today that safari is not working very well with streamed HTML. It waits for the whole document to download before it wants to render anything.

Maybe that is because I have a streaming custom element in the page but that doesn't make sense at all if Chrome is happy with what I do.

Collapse
 
tigt profile image
Taylor Hunt

I suspect that’s because Safari doesn’t support Declarative Shadow DOM yet — does the page stream fine without the custom element?

Collapse
 
yw662 profile image
yw662 • Edited

I tried adjusting things and find that the document itself is supported very well.

The real issue is, (on safari), If I modified style or class of an element, when it is still streaming, the style change won't happen until the element is complete. I am not sure it is just style and class, or it is I just cannot set and attributes.

Thread Thread
 
tigt profile image
Taylor Hunt

Yeah, it’s kind of a long story. The gist of it though is I suspect you’re right, the streaming custom element sounds like the culprit due to historical plumbing issues. Not sure how you can work around it until Safari updates their support.

Thread Thread
 
yw662 profile image
yw662 • Edited

I do that to avoid flashing of unregistered custom element (the custom element version of fouc). And my workaround is, allow that in safari.

Thread Thread
 
yw662 profile image
yw662

The interesting part is that, I cannot even do :not(:defined){visibility: hidden} or :not(:defined){display:none}. What I can do, with safari, is,

 :not(:defined) { color: white }
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
tigt profile image
Taylor Hunt • Edited

Do opacity or filter: opacity(…) work?

Thread Thread
 
yw662 profile image
yw662

Yes that works. But really no much difference I guess.

Collapse
 
yw662 profile image
yw662

But it is not a DSD, it is a normal custom element driven by javascript

Collapse
 
peerreynders profile image
peerreynders • Edited

I believe this is why Kroger.com used a SPA in the first place — if disparate teams’ APIs can’t be trusted, at least they won’t affect other teams’ code.

I think this is an aspect that assisted SPA adoption in general. Given that at the time MPAs were relatively slow to respond (server frameworks probably played a part as well), a ready-to-go (smallish) package of JS could have been shipped to the client and started doing some useful work.

Now when discussing web performance SPA proponents often counter that tuning with respect to FCP and TTI only effects initial page load — missing the point that if page loads are universally fast you may not need that SPA - unless you're going offline-first.

Back in 2013 Ilya Grigorik talked about "Breaking the 1000ms Time to Glass Mobile Barrier" and here we are 9 years later where multi-second load times on mobile are not unusual.

While now dated (2013: during 4G adoption) he describes the complexity of sending a packet over the cellular network which increases the latency for mobile compared to regular networks (sometimes I'm surprised anything works).

Picard facepalm - all that for a single TCP packet ...

He also points out that when it comes to web page load times reducing latency matters more than increasing bandwidth (More bandwidth doesn't matter (much)).

Dust, a template language that seems to have died twice.

Patrick Steele-Idem (2014): Marko versus Dust

"Marko was developed to address some of the limitations of Dust as well as add a way to associate client-side code with specific templates to rein in jQuery, provide some much-needed structure, and provide reusable widgets. Version 1.0 was released in May 2014." from eBay's UI Framework Marko Adds Optimized Reactivity Model

I think one reason is the inconsistent name

Basically if you didn't geek out over HTTP servers and the HTTP spec you probably didn't know or think about "chunked transfer-encoding" (in connection with HTML responses). And since about 2016 online searches would funnel you to the Streams API.

Collapse
 
tigt profile image
Taylor Hunt

missing the point that if page loads are universally fast you may not need that SPA - unless you're going offline-first.

The rest of this series will essentially be illustrating “are you sure you need that SPA?”

Offline-first is usually the purview of SPAs, or at best an app-shell model bolted onto a content-heavy website. However, I was able to do some mad science with Marko… its compiled-for-server representation only has to write to an outgoing stream. You probably see where this is going. (More on that later.)


Ilya’s work definitely inspired me. Subsequent MPA interactions luckily don’t have to do the full mobile connection TCP warmup if you use Connection: keepalive, and if you have analytics pinging your origin every 30 seconds anyway, that socket rarely gets torn down. We’ll see some of my measurements around that later.


Great point about the Streams API. Maybe we need an entirely new term altogether.

Collapse
 
lili21 profile image
li.li

An network proxy could break streaming likely. if the proxy buff the response, it won't send the chunk to client/user until it receives all the chunks.

lack of CDN support is another issue I guess. Did you ever run into this kind of issue? if you did, how did you fix it?

Collapse
 
tigt profile image
Taylor Hunt

I have yet to find a CDN that doesn’t work with streaming, but apparently AWS Lambda doesn’t. (I don’t use AWS for other reasons, so this has never been relevant for me, but it may for others.)

Even if a misbehaving middlebox buffers the response, at least it’s no worse than doing the SSR all at once like before.

Collapse
 
lili21 profile image
li.li

what's about compression? does it work with streaming?

Thread Thread
 
tigt profile image
Taylor Hunt

Yep! Each chunk of the stream can be compressed — Node.js in particular was designed to excel at combining gzip with Transfer-Encoding: chunked

Collapse
 
viorelmocanu profile image
Viorel Mocanu

Oh, I can't wait for the next posts in this saga! :) I've criticized SPAs for their lack of performance, stability, SEO, accessibility etc for a long time now, and I can't wait for an actual story or someone finding an alternative to API-dependent (or "non-static server depending") rendering...

Collapse
 
tigt profile image
Taylor Hunt

I think you’ll like the (currently) fourth post, then

Collapse
 
jon49 profile image
Jon Nyman

I'm glad Kroger is working on their performance issues. I remember going there and seeing a sign that if I sign up for an e-coupon I could get a deep discount on an item and it took 10 minutes just to log in on my phone. It was an extremely frustrating experience. But since I had more shopping to do it wasn't a huge deal, but still ridiculous.

I always curse devs that don't care about performance. Especially when it is known that the customer will be on a cellular connection. They make the experience horrible for people on slow cellular connections.

And for making websites overly complex. Just keep it simple using straight up simple HTML/CSS most of time works for most sites.

Collapse
 
imthedeveloper profile image
ImTheDeveloper

This is methodical. I love it.

Collapse
 
redbar0n profile image
Magne

I couldn’t find the last part of the story:

  • Burning out trying to do the right thing
  • And by the end, some code I dare you to try.
Collapse
 
tigt profile image
Taylor Hunt

I haven’t written it yet — sharp eye!

Collapse
 
redbar0n profile image
Magne

oh, that's great. I was a bit thrown off by the "(2 Part Series)" at the beginning and end. A "To be continued..." at the end, or an "(X Part Series)" would have helped.

Thread Thread
 
tigt profile image
Taylor Hunt

Yeah, the series UI on Forem was intentionally designed to avoid showing TBA posts. I’ll edit the last paragraph

Collapse
 
kernelsoe profile image
Kernel Soe

Not sure how fast comparing to your case, there’s a streaming react framework 👉 ultrajs.dev

Collapse
 
tigt profile image
Taylor Hunt

Oh neat, this one is new to me. Do they have an example app somewhere I can point WebPageTest at?

Collapse
 
tigt profile image
Taylor Hunt

One where it’s streaming from an API, specifically . The stuff on their /examples page seems to be all static so far

Collapse
 
nfsf profile image
NFSF

Love the series so far. Some more data points related to streaming

Instagram wrote about streaming HTML in their engineering blog: instagram-engineering.com/making-i...

LinkedIn does something similar, but with API data responses; data is flushed and streamed into the DOM inside script tags. This helps to parallelize the typical SPA lifecycle of 1) load the JS 2) make API calls 3) render views once the data is returned.

Collapse
 
philw_ profile image
Phil Wolstenholme

Love these posts! Looking forward to the rest :)

Collapse
 
redbar0n profile image
Magne • Edited

«
It’s easier to show than to explain:
** content (image/gif?) misssing **
«

Collapse
 
tigt profile image
Taylor Hunt

dev.to sometimes has that happen to <video> elements, not sure why. A refresh usually fixes it for me.

Collapse
 
redbar0n profile image
Magne

ah thanks, that worked. iOS Safari here. Dev.to is not always the best with html tags in posts, I’ve noticed.

Thread Thread
 
tigt profile image
Taylor Hunt

Good to know it’s a cross-browser bug, at least; I notice it on desktop Firefox

Collapse
 
jcbbb profile image
jcbbb

Logged in just to like this :)

Collapse
 
jon49 profile image
Jon Nyman

This is my answer to streaming HTML. github.com/jon49/html-template-tag...

I've played with it in a service worker for offline use.

Collapse
 
ruyili profile image
Ruyi Li

Very interesting read! What are your thoughts on Remix (remix.run/) as an alternative to streaming? Sorry if this is a silly question, I'm still a novice with regards to web performance.

Collapse
 
tigt profile image
Taylor Hunt

I was asked this question on Twitter too, so you may be interested in the answer there. TL;DR: I’m not sure it would be as fast, but I haven’t tried it myself and measured yet.

However! You may not have to choose. The Remix devs’ replies on Twitter suggest that Remix will probably support streaming once React 18 releases.

Collapse
 
mattstone profile image
Matt Stone

Super interesting article (and series), thanks Taylor!

Collapse
 
gabrielfallen profile image
Alexander Chichigin

Great post! I like your style and approach. :D

I have to admit I wasn't too attentive, but this thing reminded me about hotwired.dev/ and all the libraries on that page. Maybe you'll find it useful for something.

Collapse
 
tigt profile image
Taylor Hunt

Hotwire’s Turbo Streams sound similar, but they’re surprisingly different — they’re JS-powered ways to do in-page updates from a subresource. Streamed HTML is a no-JS technique that works for the very first round-trip.

Collapse
 
gabrielfallen profile image
Alexander Chichigin

Indeed. They just look similar to that <await> tags. And the overall "use as much good 'ol HTML as possible" attitude. :)

Thread Thread
 
tigt profile image
Taylor Hunt

Yeah, the attitude itself I am all in on.

Collapse
 
johnbetong profile image
John Betong • Edited

Why bother with the additional overhead of JavaScript when CSS now does the job with:
<img src=“…” loading=“lazy” alt=“#”>

Demo to be tested with tools.pingdom.com/#5fed249586400000 shows 4.4MB page load in a fraction of a second!

this-is-a-test-to-see-if-it-works....

Collapse
 
peerreynders profile image
peerreynders

The loading attribute on the <img> image embed (HTML) element deals only with the deferred loading of images.

The article discusses the streaming of the HTML page which typically means flushing the top part of the page early while the server is still assembling the bottom part, perhaps waiting for results from one or more service APIs.

That way the browser can process the document meta data (<head>) element and to start downloading any additional resources required, perhaps even offer some "top of the page search functionality" before the rest of the HTML page has even finished loading.

Marko goes even further by supporting progressive rendering with async fragments.

An example from as far back as late 2014 demonstrates this approach.

Collapse
 
adilbaaj profile image
Adil Baaj

Hey, apparently this is possible now with NextJS with server components (nextjs.org/docs/app/building-your-...)

Collapse
 
ryannerd profile image
Ryan Jentzsch

Marko just looks like PHP/Laravel's Blade boilerplate.

Collapse
 
silverium profile image
Soldeplata Saketos

Finally NextJs 13 implements it in React! nextjs.org/docs/advanced-features/...