Hacker News new | past | comments | ask | show | jobs | submit login
Streaming HTML out of order without JavaScript (lamplightdev.com)
226 points by goranmoomin 56 days ago | hide | past | favorite | 98 comments



At the core, this seems to be about shadowrootmode=open, a new html feature which already landed in Chromium and Safari:

    <div>
      <template shadowrootmode=open>
        This is <slot name=a></slot> and <slot name=b>
      </template>
      <span slot=b>sunny</span>
      <span slot=a>funny</span>
    </div>
renders as:

    This is funny and sunny
So it looks like HTML got a bit of a native template system now.

What I have been wanting for decades is a native template system in HTML which supports urls. Similar the the script tag, but which loads html:

    <html url="/header.html">
    <html url="/content.html">
    <html url="/footer.html">
I always wondered why this was never implemented. It seems such a basic functionality which would allow to comfortably build websites in a modular way without scripts or serverside processing.


https://www.w3.org/TR/xinclude

The W3C specced something out around 20 years ago, but browser devs are allergic to all things XML.


Because it doesn't have to be xml.

I agree partials should be a native feature.


[flagged]


What? This feels like a largely unrelated rant.


The last example reminds me of the once-proposed html imports feature. It was being pushed by Google back when Polymer was a thing and would've given us actual web components delivered as html documents with full support for html, css, and js just like we use normally. If I remember correctly there were a few issues with the approach that needed to be resolved and nobody stepped up to make it a reality. So instead we got the custom elements spec years later which only implemented a small percentage of what made html imports useful.


In the end it was only Firefox who implemented HTML Imports, and Google has gone mad and pushed everything in to Javascript.


That is not what happened at all. Firefox never shipped HTML Imports.


Well I'll be! In my mind I had this clear picture of Firefox implementing it.

It correct, it was only Chrome: https://caniuse.com/?search=html%20import


They went Golden Hammer on v8.


xslt is a native template system that has includes: <xsl:copy-of select="document('/header.html')"/>

xhtml/xslt still works today (though browsers are stuck on v1). It's a declarative template system with a pretty powerful pattern matching system (xpath). You can also extract fragments from external documents or store them into variables. e.g. <xsl:copy-of select="document('/content.html')/body/div[2]/ul/li/a/@href"/> will copy all urls from a list inside the 2nd div. Or instead of second div in the body, you could search for a div with an id or specific attributes or specific children or whatever pattern you want.

Why webdevs forsook xml will always be a mystery to me. Particularly given that they now use things like JSX.


I can only speak for myself, but the few times I encountered xslt in the past it was in the context of bulky and verbose SOAP services. The syntax felt off to me (and still do).

I think this was at the time I was learning jquery and using css selector syntax for finding elements, and the difference between that and xpath was pretty stark.

I basically (probably with insufficient knowledge) filed xslt away with vb and tables for formatting as old, complex and soon to be replaced


The syntax felt off to Juniper, too, so they made their own: https://juniper.github.io/libslax/slax-manual.html. Loses homoiconicity to some extent, sure, but outputting XSLT using XSLT never was particularly pleasant.


I can confirm that SLAX is a pleasure to use. Pity it hasn't had adoption outside Juniper to my knowledge.


I just recently moved my personal homepage to XML + XSLT. Not a pro at it at all though. I did enjoy the process, I like the idea of building the website out of data + templates, but that's not unique to XSLT.

Besides some papercuts / browser bugs like "refreshing the page desyncs the inspector", the main issue I find with it is I don't see how I can modify things dynamically?

By the time JS runs, it's on the resulting HTML DOM, not the XML source, so I can't add elements dynamically respecting the XSLT stylesheet?

Also, XML must be well-formed and have a single root, I can't stream it out on the server piecewise?

With those 2 limitations, I don't see any advantages over any other server-side templating language, it just duplicates work that could happen once in a SSG or at least cached in a CDN, onto every client computer.


Right, unless you run a javascript xslt processor, you can't do things after page load. There's at least one javascript implementation that lets you do data binding to do dynamic page updates, but I'm not familiar with it.

The main advantage for simple things is that you don't need an application server or compiler. It's all static files, and it's all built right into the browser so easy to get started with. For less simple things, it should be easier to cache since you can separate out user data and have the bulk of the information you send be the static template (which the user can cache).

I suppose maybe that last point is why people use javascript frameworks for static pages (so they can send a static, cached js file and then small payloads for user data), which seems like overkill to me.

It would be nice if browsers supported streaming xml. Of course streaming xml exists (e.g. XMPP). It's not really any different than json: the spec said you need to have a single root object, and then someone wrote a spec that says you don't need to do that.


Ok, thanks, then I'm not missing something obvious, heh.

You make a good point of caching the XSLT file itself though, I hadn't considered that. Simple server outputs XML data, browser renders into a page.


Firefox supports declarative shadow DOM too now!

There's a long-standing WHATWG feature request open for client-side includes here: https://github.com/whatwg/html/issues/2791

And several userland custom element implementation, like https://www.npmjs.com/package//html-include-element

One of the cool things that you can do with client-side includes and shadow DOM is render the included HTML into a shadow root that has <slot>s, so that the child content of the include element is slotted into a shell implemented by the included HTML.

This lets you do things like have the main page be the per-page content and the included HTML be a heavily cached site-wide shell, and then another per-user include with personalized HTML - all cached appropriately.


> This lets you do things like have the main page be the pre-page content and the included HTML be a heavily cached site-wide shell, and then another per-user include with personalized HTML - all cached appropriately.

If you can do your includes at page load time, you've been able to do this for 25 years with xslt; it's been built into the browser almost since the beginning of the web!

I've played around with doing exactly that: include a `/myinfo.xml` document that has information about your user, and then the rest of the page template just grabs `$myinfo/user/@name` or whatever wherever it needs. The neat thing is it has graceful degradation by default: if the request fails, then your include will be empty and you can treat it as a logged out page. So you can e.g. display the username and logout button in the top right if logged in, or a login button if not. You can also e.g. include a CSRF token in your info document and plop that into any forms in your page by just doing `value="{$myinfo/csrf-token}"` or whatever.


The "template" use case was a very common one from iframes/framesets in the early 2000s, and the markup often looked a lot like your desired example.

My memory's hazy, but I remember it becoming more and more complex to maintain as the web's security and performance model evolved, as you needed to manage and secure all of these disparate, discrete DOM trees in the same page context.

Wouldn't what you suggested just been "reinventing" frames/framesets if you added the ability to arbitrarily pull in templates via the network?


> I always wondered why this was never implemented

Wasn't this in HTTP/1.1 (1997), where you could send chunks to anything that could have a target attribute? (As multipart HTML relied on http chunks, which suffered poor support by the Windows http stack, this quickly vanished and in the 2000s support was stripped from browsers. I think, Chrome was the first to do so. Personal note: I once wrote a chat server, which relied on this technology – and it worked pretty well, outside IE.)

Another way this had been once supported was in Netscape 4.0's layers (1997), which could have a `src` attribute and worked just as you would expect. (However, support for this vanished with the first iterations, about the same time JS-styles were switched to CSS. It had been definitely a feature in the NS 4.0 prerelease versions. Especially the capability to re-link the `src` attribute by JS or by a link target was nice.) NS 4 also featured extended HTML entities, which could render JS expressions in place and also provided a mechanism for conditional comments – so, had we followed that route, HTML would have been already a templating language.


Probably because HTML specs were quasi frozen for a long time. As another comment said, XSLT supports it. XHTML conformance would have made the web development world stricter, and more flexible. Thank you Microsoft for holding the web back.

There are also [Server Side Includes](https://en.wikipedia.org/wiki/Server_Side_Includes) which were used for that.

Funny enough, I've used SSIs first and last time around 2020. The project I worked on had a heavily cached landing page (for performance reasons), but needed a way to swap out the homepage banners based on product releases, and the easiest way around that was to output a SSI directive for a separate banners endpoint.


The lack of native HTML includes is a big reason why I picked up just enough PHP to write functional pages back in the early-mid 00s. Not having to manually propagate changes to e.g. navbars across a whole site was mind blowing.


We had HTML imports a decade ago https://web.dev/articles/imports

This would have been better than world peace. But this threatened a lot of warmonger advertisers. So it was "deprecated" and then obliterated.


It seems supported in most browsers, not just Chromium and Safari: https://caniuse.com/mdn-html_elements_template_shadowrootmod...


This looks exactly like svelte named slots syntax: https://blog.logrocket.com/comprehensive-guide-svelte-compon...


SPA like React, Vue, Angular, Svelt, solid, should be not needed if HTML implemented these features. Or make JSX a standard again such as E4X.


I’m all for standardizing JSX, but I think we should be clear that its only similarity to E4X is syntax. The latter corresponds directly to the browser/HTML/XML DOM, whereas the former corresponds to nothing in particular except the semantics you might give it. I’d also be interested in a revival of E4X as a potential implementation of standardized JSX, but I’d be disappointed (and I think many JSX users would be too) if the standardization coupled the two completely.


All of the libraries you’ve listed offer features above and beyond what we’re talking about here. Plus, web components exist, they do most of it and yet the libraries still exist.


See: $ becoming querySelector.


Yes, many frameworks including svelte took heavy inspiration from various parts of the Web Components specs, including named slots. So, this is the original thing that svelte and others copied.


And that's not a coincidence; svelte chose to borrow that concept from Web Components.


It's also in firefox, at least in 123.


https://www.w3.org/TR/html-imports/

There are long and boring reasons why it never went anywhere.


That was like what ESI (Edge Side Includes) allowed from Akamai. Perhaps had the founder[1] of Akamai not been killed in the world trade towers we'd have had that in the standard...

[1] https://en.wikipedia.org/wiki/Daniel_Lewin


<frame> is already there


That's not what the op is asking for, iframes are in their own self-contained context.


While it needs a little JS to initialise, that’s easy enough to do with custom elements: https://github.com/gustafnk/h-include


So... iframes that stay in the context of the parent element? You'd need some tight CSP coupling to prevent it from being a security disaster.


Not sure how it would have any concerns greater than importing JavaScript files.


Includes are different than this. Pretend the template is site header, site nav, body content, and footer; each being a slot. The server then streams body content, site header, site nav and footer in that order. Now more important content gets rendered first without any JS.

This technique would be a lot more usable though when shadow roots become open style-able. It is kinda funky having to apply your CSS separately to the layout regions that live in your shadow dom.


You can have a global stylesheet and @import it inside Shadow DOM or use ::part() from outside of it.


I used to do a lot with Apache server side includes and always wished there was a client side option.


It reminds me of FrontPage extensions, "good" times...


Native partials should be awesome


can be done using service workers or workbox(abstraction to sw)


Real fun begins when you realize that the streamed content can react to interactions that user submits from the streamed page: form targetted into hidden iframe or any other mean of issuing HTTP request back to the server and handling data it can react to in next streamed chunk. This way you can have for example "CSS only chat" [1], or basically anything, since the chunk with new data can contain style directives hiding the old data…

[1] https://github.com/kkuchta/css-only-chat


If I add multiple slots with the same name it shows all of them.

    <div id=t>
      <template shadowrootmode=open>
        <slot name="s1">s1</slot>
        <slot name="s2">s2</slot>
        <slot name="s3">s3</slot>
      </template>
    </div>
    
    <script>
      setTimeout(_=> t.innerHTML += `<span slot="s3">three</sp>`, 1000)
      setTimeout(_=> t.innerHTML += `<span slot="s1">one</sp>`, 2000)
      setTimeout(_=> t.innerHTML += `<span slot="s2">two</sp>`, 3000)
      setTimeout(_=> t.innerHTML += `<span slot="s1">ONE!!</sp>`, 4000)
    </script>
I guess there is no way to replace a slot, again and again?


You can hide the previous slot contents with CSS: https://jsbin.com/rinevokiwi/edit?html,output


That's cool and definitely the future of HTML streaming,

it simplifies things a lot: js enabled out-of-order streaming leads to SEO problems and frameworks usually need to come up with workarounds - detect bots and turn of streaming for that case.

With such technique no workaround is needed, less things to worry about.

Excellent!


This might lead to SEO problems as you wont know what the SE will see rendered i it’s text only view.


Fun stuff!

The underlying `swtl` library is itself pretty interesting.

Note the use of `delayed(...)` in the blog's example code: a promise in a tagged template.

SWTL's `html` allows for this (I think the blog's example follows the simple case here https://github.com/thepassle/swtl/blob/main/html.js#L23). And SWTL's `render` method races the promises (https://github.com/thepassle/swtl/blob/main/render.js#L120) outward.


You could possibly do this using a flexbox?

https://developer.mozilla.org/en-US/docs/Web/CSS/order


Flexbox order doesn't affect accessibility tree order, so that's bad for accessibility. DOM order should make semantic sense.


There is a new reading order attribute coming to fix this.


There already is one in ARIA that supports this: aria-owns [1] and to some degree aria-flowsto [2]

But with everything in ARIA, it always depends on real-world support of screen readers, which is very poor. You have to work with what actually works unfortunately.

Imo changing order just to change the streaming order for a tiny performance gain is not worth breaking the UX for people with accessibility needs.

[1] https://developer.mozilla.org/en-US/docs/Web/Accessibility/A... [2] https://developer.mozilla.org/en-US/docs/Web/Accessibility/A...


Why wouldn't they just amend the existing feature so it's accessible?


Backwards compatibility I'd assume. Someone could have taken advantage of the fact they flex order and accessibility order are different.

Changing the functionality now would break those sites, adding a new CSS prop would make the fix opt-in.


Managed tabindex would solve that. Sounds like a nightmare to maintain though.


tabindex only affects tab order (of interactive elements), not the order that a screen reader navigates and reads out content (including non-interactive elements)


And how is <slot> for accessibility?


Only if the elements are siblings


This technique of out-of-order streaming using DSD was shown for the first time by lamplightdev on Jun 28, 2022: https://twitter.com/wcbytes/status/1541546418539732997

Demo: https://enamel.pages.dev


Let's say I streamed the first chunk with a little JavaScript, would that JavaScript be able to listen to future chunks coming in? In a real application it is crucial to be able to update your frontend state once new data is ready.

First thought is [mutation observer](https://developer.mozilla.org/en-US/docs/Web/API/MutationObs...) but that seems too deep into the weeds


Another approach would be to correlate observation with specific custom elements. You can define them in the first chunk, then use their connectedCallback method to observe them as they arrive. The benefit of an approach like this is that the custom element lifecycle APIs are synchronous (which is also the drawback of the approach, and where MutationObserver is more appropriate if that synchrony is a concern).


Perhaps the most horrible JavaScript I ever wrote did something like this, and has been very useful for over a decade and still going strong.

It's a log file that starts with a header: just a script tag with some JS, followed by a special string (I used a special HTML comment as a boundary indicator). The server then appends its logging to this file in a web hosted directory.

The JS periodically does an XHR of it's own location.href, polling itself. It then splits on the special boundary string, thus collecting the latest log data, parses it to generate pretty/coloured/linked HTML, and updates the current page according (and controls scrolling if required).

This gives you a log file that's continuously being appended to, but can be visited in the browser using any static file serving and automatically updates as it's populated.


Mutation observer is pretty easy once you get used to it, just look for data you care about and ignore the rest.

Alternatively, I think slotting raises an event, so you could use js to remove the pre-existing content when new content gets added


I'd assume that the only reason one would need to know if chunks were incoming is to indicate a loading state in disparate parts of your webpage, e.g. chunk B has some widget that indicates chunk A is loading. In this case, you can probably work off the assumption that loading is the default initialization state, and use JS to communicate to the disparate parts of your page once on chunk A load. There may be something different that you're thinking about here though.


Very cool. Perhaps this could be used for non-JS CAPTCHAs, since it would force the bot to wait for a few seconds.


Neat, but what would be the intended use case for something like this? I can imagine a scenario where one part of the UI may take extra time to load, so you would defer it to let the other parts load first. Is there another reason I'd want this?


I used similar streaming HTML for my RSS reader. When you import subscriptions it can take a while as each feed is fetched so I use streaming HTML to show the progress of each feed as it is imported. This provides meaningful progress updates to the user.

However previously the limitation was that the content needed to be more or less in order (you could use some CSS tricks but they were limited). Using this trick I would be able to render the full list of pending feeds then insert a result as each finishes being fetched.

In fact it seems that for each slot the last element will be used. So you can even create live-updating pages based on this which is really cool. For example imaging you had a score ticker. You could push an update to that region of the page every time the score changes.

I think this is definitely a niche use case, but it is very nice to support this without JS. Of course in all but the simplest use cases JS may still be the right solution. For example if the connection drops for any reason there is no mechanism for showing the user an error, let alone retrying.


I was just trying that out, I _think_ it just concatenates all the content into the slot, but maybe there's a way to do that?


In that case, just use a selector like `.content-item:not(:last-child)` to hide everything but the last item that was added.


Dealing with loading in a smart way (even small amounts of loading), without extra JS overhead, is the fundamental use case.

For a comparison, basically the entire bajillions of man-hours that have gone into React Server Components [1], and all the many, many complications and footguns inherent in their approach, is trying to solve the same problem.

[1]: https://nextjs.org/docs/app/building-your-application/render...


This brings the memory of BigPipe, facebook has had this years ago (using JS of course).


This helped me with a 'visual' understanding of what's happening, go to

view-source:https://ooo.lamplightdev.workers.dev/

then watch the bottom of the page as the new `<span slot=...>` elements come in. As they come in, the browser's, well, slotting the contents into the corresponding slot in the template.


This is a long overdue but promising trend toward a JavaScript-less website.

Yeah, I am one of the few outliers who still host JS-free out of cybersecurity reasons.


This looks very interesting, I was not aware that this was possible natively without JS.

What's the browser support like?


Chrome, Edge and Safari since March 2023; Firefox since February 2024.

https://caniuse.com/mdn-html_elements_template_shadowrootmod...


Seems like browser support is pretty universal, even says so in the article

> All browsers support streaming HTML

And the caniuse is promising: https://caniuse.com/?search=slot


<slot> has been around for a while in Web Components; the relevant feature here is probably the (much newer) shadowrootmode attribute: https://caniuse.com/?search=shadowrootmode

But you can already do a lot with regular streaming and using CSS Grid, inline <script> tags, etc to move stuff around the page.


The site loads pretty slowly for me [1], and when they site has fully loaded the list has already been completed, there is no streaming from what I can see. Is Firefox the reason here?

1. https://check-host.net/check-report/16234f41keed


The site[0] works for me in Edge, and Firefox, as intended. The slow loading is the point, you should see how it updates real time, which doesn't happen to you for some reason. As other comment suggests, maybe something on your network doesn't like tiny (~42 characters) HTML fragments.

[0] : https://ooo.lamplightdev.workers.dev/


Firefox just added support for declarative shadow dom, so that might be it if you're not on the latest version.

Alternatively I could imagine some kind of proxy between you and the site buffering things up.


> some kind of proxy between you and the site buffering things up.

Yeah I think that might be it, because it works fine on FF (iOS) outside.


Also for me, both on Firefox and Edge - I'm wondering if it's to do with being behind a corporate proxy that MITMs everything and might be buffering rather than passing through partial respones, or has a much higher threshold before streaming?

If that is the reason, it'll provide an (even more than usually) poor experience for those behind such a proxy


It worked fine for me in Firefox.


The big application for this is multicasting html via UDP. P2P web apps could leverage this.


[flagged]


The Javascript code in the article is server-side and not essential - it's just one way to take advantage of open shadowrootnodes and it could be written in PHP or anything else. It's showing how you can output your HTML in a very different order, with arbitrary delays, and still get the page layout you expect.


Javascript is being used to stream the HTML. the title is "Streaming HTML out of order without JavaScript"


if only it was "Consuming streamed HTML out of order without JavaScript"...


> It's worth noting though that there's no dependency on a JavaScript backend - the same thing can be achieved on PHP, Java, Go, etc.

The only Javascript in the article is on the server side to handle the HTTP.


Javascript is being used to stream the HTML. the title is "Streaming HTML out of order without JavaScript"


[flagged]


There are things that are slow that can't be made to be fast. And there are even more that CAN be made fast but only at extreme cost.


The downside here is the need to proxy everything through the same server. That will add latency and possibly throttle bandwidth if that data is coming from third parties.

The scenario where this could be good is something where there are background jobs running behind the service that would have been API-ed through the same web server anyway.

But I have an ingenious solution for the best of both worlds! An Iframe.


Let's come back to this once we've stopped writing software that is 90-99.9% waste (in round trips and in cycles).


I hear people complain about this a lot. But people who write software that is that efficient are 90-99% wasting their precious time on Earth.


My bad, I finally realized it's better to spend much more time as long as it belongs to other people.


If you go bankrupt before you even get to market, then you aren't wasting any CPU time of anyone else :P




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: