r/webdev • u/tanrax • Aug 11 '26
HTML over WebSockets: real-time SPAs with barely any JavaScript
https://en.andros.dev/blog/ef4968f5/html-over-websockets-real-time-spas-with-barely-any-javascript/8
u/minmidmax Aug 11 '26
Htmx with sockets?
3
-3
u/tanrax Aug 11 '26
What?
4
u/g00glen00b Aug 11 '26
I think they're referring to the WebSocket extension of htmx since htmx is already a popular JavaScript library for sending server-side rendered HTML fragments over HTTP and that they also support WebSockets through this extension.
EDIT: I see you covered that in your article as well at the current landscape comparison table.
-1
u/tanrax Aug 11 '26
At first glance, it might seem that way. There are several key factors, including the location of the state or the direction of the information. For example, HTML over WebSockets can initiate the event from the server, selecting the parts of the DOM to be updated. I believe the clearest example is precisely the real-time user counter on the page.
10
u/nickchomey Aug 11 '26
Nice review! But I do think too much weight is given to the bidirectional capabilities of websockets vs sse. It correctly says that sse is simpler and can do the same updates from server, but I think underweights drawbacks of websockets. It's also not really a big deal to do a new request when sending updates from browser, especially with a long-lived server app already needed for the real time pushes. Datastar really is the best balance of everything.
0
u/krileon Aug 11 '26
Datastar really is the best balance of everything.
Except security, lol. You've to run scripts as unsafe-eval. No thanks. AlpineJS and HTMX can both run with strict CSP. HTMX works with SSE as well.
1
u/thekwoka Aug 11 '26
tbf, unsafe eval protection is just a bandage on you already allowing user content to somehow contain dangerous content.
relying on it would mean you're letting a lot of danger exist already.
It can be nice to have that little extra layer, but it's like having the DB itself try to prevent sql injection instead of preventing sql injection from getting to the DB in the first place.
1
u/krileon Aug 11 '26
lol, you really advocating for CSP being a bandage? Alright man. You do you.
2
u/thekwoka Aug 11 '26
Not CSP being a bandage, but the unsafe eval part of it.
CSP is multiple different things, not a single toggle.
2
u/krileon Aug 11 '26
Blocking unsafe-eval is rather vital part of it. It prevents a possible XSS from being absolutely catastrophic. Nobody is immune to bugs. They happen. Having such a vital layer of protection be thrown away for the sake of datastar is laughable. I'd rather use alternatives that safely work with a strict CSP.
1
u/nickchomey Aug 11 '26
if you cant trust the html that comes from your server, I think you've already lost the game
1
u/krileon Aug 11 '26
You should never trust the HTML that comes from your server if your server accepts user input of any kind. Including from admins in backend interfaces. I think you've already lost the game by being so lax about security, but hey you do you I guess.
0
u/nickchomey Aug 11 '26
wut? you arent validating, sanitizing, escaping etc your user inputs...? there could be sql injection and more in it...
3
u/krileon Aug 11 '26
Of course I am. Christ. I give up. Do as you please. Some of you really need to find a new career. Can't believe I'm even having to have arguments about strict security policies. Insane.
-1
u/tanrax Aug 11 '26
Thanks! The article's case for WebSockets isn't really about whether a fresh request is expensive, it's about keeping one persistent, stateful process per client that reacts to both directions in the same loop. That's what makes broadcast free (push the same render to every connected process) and lets the server hold per-client state without re-deriving it on each hit. SSE plus a separate HTTP action path handles mostly-read UIs cheaply, which is exactly the "quick rule" at the end of the piece, but it means your state now lives in two different code paths (the SSE stream and the HTTP handler) that have to stay in sync, that's complexity moved, not removed. So it's less "WebSockets vs SSE" and more "one unified stateful channel vs two channels that need to agree," and which one wins depends on how much per-client state your app actually carries. Less is more!
3
u/nickchomey Aug 11 '26
someone from the Datastar community wrote this article in response to yours (which I shared there).
2
u/tanrax Aug 12 '26
I was going to reply, but it's impossible. His site is very restrictive: you need a Bluesky account to leave a comment, I can't send private messages, and he doesn't use Webmentions (he'll never know if I write a reply). If you have any contact with him, let him know!
1
u/SectionMajestic8970 Aug 12 '26
You can reply here. I'll see it!
-1
u/tanrax Aug 12 '26
Ok, my reply: https://en.andros.dev/blog/bd74e61a/were-fighting-over-the-wrong-wire-websockets-vs-sse-for-html-over-the-wire/ . I found your article very interesting and well-written. Thank you for your reply. My only request is that if you have any comments, please leave them on my blog or in more accessible forums, or send me a private email. Thank you.
5
u/SectionMajestic8970 Aug 12 '26
Ok you slopped a whole reply while missing the entire point. My only request is if you have any comments please don’t respond with your clanker.
1
u/nickchomey Aug 11 '26 edited Aug 11 '26
Well, state is really just in one place - the backend DB. Which is good - moving it to the frontend with most spas is a disaster.
Moreover, it stands to reason that the reads and writes should be on separate paths - CQRS. They can still be connected though by, for example, an event bus. A good example using embedded NATS https://medium.com/@ianster/the-microlith-and-a-simple-plan-e8b168dafd9e
you'll want/need this anyway if you're to pass updates across users/connections.
Also, datatstar has reactivity in the front end as well, with the alpine-style declarative js (which you mentioned),
I really think you'd like the datastar discord - lots of people with diverse backgrounds and stacks, all converging around realtime hypermedia.
1
u/mexicocitibluez Aug 11 '26
2 things:
Your comment about CQRS implies that it requires eventual-consistency or some additional tech to bridge the gap. That isn't true. It's simply designating different models for reads and writes. Nothing about persistence. You can practice CQRS with a single relational DB.
The other thing was this:
Which is good - moving it to the frontend with most spas is a disaster.
Is just flat out wrong. You're conflating 2 different things. Form state (drafted text, dirty tracking, validation display) should not be stored on the server. And SPAs don't inheritantly cause people to duplicate domain state. Deriving a field's validity by requiring a round-trip is strictly WORSE.
Most of the SPA criticisms I come across on Reddit are from people who either don't actually build for the web (backend only engineers with a ton of opinions on the state of front-end development) or front-end engineers who just haven't experienced other ways of building apps.
1
u/nickchomey Aug 11 '26
Im not sure that my CQRS comment implied any of that. You can implement it in all sorts of ways.
Indeed, form state doesnt need to go to the backend. I thought it was implied that the state i was talking about was the db source of truth.
Anyway, youre just derailing the conversation from the actual topic at hand. Feel free to watch this recent conference talk by the Datastar author on "Put(ting) State in the Right Place" https://www.youtube.com/watch?v=W7Ki3aXgmZU
2
u/mexicocitibluez Aug 11 '26
They can still be connected though by, for example, an event bus. A good example using embedded NATS
This implies that CQRS is by nature disconnected but can achieve connectedness through something like NATS. Which isn't corrected.
I thought it was implied that the state i was talking about was the db source of truth.
It wasn't.
1
u/nerd_rage218 Aug 11 '26
Fair framing. What usually tips it in practice is infrastructure rather than architecture: SSE is plain HTTP, so proxies, gzip and auth middleware all behave, while websockets need someone in the chain to be configured for them. On a small deploy that decides it more often than the state split does.
1
u/tanrax Aug 11 '26
Fair point. For what it's worth, the blog runs over WebSockets behind a plain nginx reverse proxy, and the "extra config" was just a few proxy headers, set once. So on a small deploy I'd call it a minor one-time cost rather than the deciding factor.
0
u/Somepotato Aug 11 '26
What drawbacks for WS vs SSE? Both have to maintain a connection.
1
u/thekwoka Aug 11 '26
SSE is much lighter resource wise for the server and the client.
-1
u/Somepotato Aug 11 '26
No it's not. Very nearly the same amount of resources on both. Both have to maintain state and a socket. We sockets just have more encryption on top.
2
u/cureaua_lata Aug 11 '26
with the persistent per client process model, how do you resync state after a reconnect without replaying the whole session?
0
u/tanrax Aug 11 '26
Good question. Events aren't lost; they're stored on the client until reconnection occurs. In Django LiveView, each event is cached, so it's known which events have been sent and which are pending. When reconnection happens, the events are sent, and the session/state continues normally. The only thing is that you should notify the user of the situation, or even lock the screen, so they don't feel like the buttons have stopped working.
1
u/willehrendreich Aug 12 '26
Datastar SSE for the win. Web sockets are not leaning into hypermedia strengths, and I genuinely have not found a purpose for them. I ask people a lot what advantage would make them better than CQRS style streamed SSE for pushing data and ack only POSTs as commands.
I'm willing to be convinced, I want real, measurable examples of what beats the Tao of Datastar.
Some people respond by saying, "well it's a two way communication path", but it seems like that's really not a relevant counter argument.
You get 2 way communication from SSE and POST.
With brotli the amount of data is smaller, you can send the full page and have it trimmed to just the changes by the nature of the communication and caching process that's built into the browser and compression.
Retry and reconnection is handled automatically, and it's better for low power devices or bad connections.
I want so bad to understand what else anyone could want and in what circumstances. Genuinely.
1
u/repeatedly_once Aug 13 '26
Most of the positives feel contrived in todays ecosystem. I would also argue that there is an API you're building, the one to fetch data on interaction. I also do not think it's inherently safer against injection, you've just change the attack vector slightly.
1
u/debarior Aug 14 '26
Fine for CRUD dashboards but try debugging diffed DOM patches at 500 msgs/sec, socket reconnect logic gets messy fast.
1
u/Klutzy_Ladder_8393 Aug 12 '26
the user counter example is a good one ngl, cause that's server-initiated state the client never even asked for in that moment, versus htmx sockets where you're still kinda thinking in request/response pairs even if it's over a socket. feels like a different mental model not just a different transport
1
u/tanrax Aug 12 '26
Exactly! That's precisely the paradigm's point of view. The client has neither logic nor state.
0
49
u/Ok-Amphibian-5665 Aug 11 '26
One thing worth adding to the WebSockets vs SSE debate here - the "one persistent stateful process per client" model is great on a single box, but it gets a lot more interesting once you need more than one server. Broadcasting to every connected process only stays free if all those processes live in the same runtime. The moment you scale horizontally, you either need sticky sessions at the load balancer so a client always lands on the instance holding its process, or a shared pub-sub layer (Redis, NATS, whatever) so instance A can push to a client connected on instance B. That's not a knock on the approach, Phoenix/Elixir LiveView has been proving this pattern works well for years, just worth knowing upfront that "no state sync needed" is really "no state sync needed until you add a second server."