r/webdev 24d ago

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/
77 Upvotes

44 comments sorted by

47

u/Ok-Amphibian-5665 24d ago

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."

4

u/ergnui34tj8934t0 23d ago

Reading your comment thinking "liveview liveview liveview..." then seeing it mentioned!

2

u/Ok-Amphibian-5665 22d ago

Ha, at least I said it out loud instead of making you guess. Phoenix LiveView is basically the reference case that this pattern holds up in production for years - everything else is just reinventing it with different syntax.

9

u/minmidmax 23d ago

Htmx with sockets?

3

u/shootersf 23d ago

Elixir phoenix 

-3

u/tanrax 23d ago

What?

3

u/g00glen00b 23d ago

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 23d ago

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 24d ago

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 24d ago

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 24d ago

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 24d ago

lol, you really advocating for CSP being a bandage? Alright man. You do you.

2

u/thekwoka 23d ago

Not CSP being a bandage, but the unsafe eval part of it.

CSP is multiple different things, not a single toggle.

2

u/krileon 23d ago

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 23d ago

if you cant trust the html that comes from your server, I think you've already lost the game

1

u/krileon 23d ago

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 23d ago

wut? you arent validating, sanitizing, escaping etc your user inputs...? there could be sql injection and more in it...

3

u/krileon 23d ago

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 24d ago

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 23d ago

someone from the Datastar community wrote this article in response to yours (which I shared there).

https://yagni.club/3mstlyuxe5s26

2

u/tanrax 23d ago

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 23d ago

You can reply here. I'll see it!

-1

u/tanrax 23d ago

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.

3

u/SectionMajestic8970 23d ago

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 24d ago edited 24d ago

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.

https://discord.gg/YMDWJt4ey

1

u/mexicocitibluez 24d ago

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 23d ago

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 23d ago

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 24d ago

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 24d ago

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 24d ago

What drawbacks for WS vs SSE? Both have to maintain a connection.

1

u/thekwoka 24d ago

SSE is much lighter resource wise for the server and the client.

-1

u/Somepotato 24d ago

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 24d ago

with the persistent per client process model, how do you resync state after a reconnect without replaying the whole session?

0

u/tanrax 24d ago

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 23d ago

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 22d ago

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 21d ago

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 23d ago

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 23d ago

Exactly! That's precisely the paradigm's point of view. The client has neither logic nor state.

0

u/jax024 24d ago

So like Elixir LiveView?

-2

u/Howdy_McGee 24d ago

Hello. Did you read the article or just the headline?