HTML over WebSockets: real-time SPAs with barely any JavaScript

Leer en español

Building a SPA (Single-page Application) is a complex puzzle: a JavaScript framework that draws the view, an API serving JSON, and 2 independent codebases forced to understand each other through contracts. It is an accepted, professionalized scenario. But being a standard does not make it the only way. I want to show you another approach, one that is not new but has gained traction over the years: HTML over WebSockets.

The idea is this: instead of sending JSON and assembling the HTML in the browser, the server sends the HTML already built and the client just places it where it belongs. All the rendering logic stays in the Back-End, in a single language, with no need for contracts or an API. This pattern is known as hypermedia or HTML over the wire. What matters about how the HTML travels is that it determines the latency and the bidirectionality of the communication. There are three variants:

The channel is so important that it determines the application's architecture and its communication pattern.

In this article I am going to talk about HTML over WebSockets: the real-time and bidirectional variant of the family. The one that lets you build a SPA with barely any JavaScript, in a single language, with no contracts and a single rendering engine. We will see what it is, how it works and when it pays off compared to its HTTP or SSE cousins.

Origin

Chris McCord, creator of Phoenix (the most popular framework in the Elixir ecosystem), presented at ElixirConf 2019 a technology called LiveView. In just 15 minutes he built a Twitter clone that worked in real time without adding any rendering JavaScript or a popular framework (React, Angular, Vue...) to manage the View, proving that you could stay in the Back-End and be productive with a sweet hint of good performance. Since then the solution has grown popular, inspiring other developers to build HTML-over-WebSockets implementations in other languages. You can go back to the Back-End without giving up the good parts of the Front-End.

How does it work?

Even though it might not seem so at first, JavaScript is used on the client. Its job is not to render but to create a communication channel with WebSockets and place the received HTML in the right spot. Plus other secondary tasks like animations, event handling, etc.

McCord's solution is not to send the Front-End a JSON, but HTML that needs no preprocessing. That way we move the rendering load, and all its logic, to the Back-End. OK but... how do we get the server to send us new content immediately and without making a request? Easy: with WebSockets.

Let's review the traditional system from the introduction. From the web I make an HTTP request, the browser starts the action and gets a JSON with all the raw information in response. The next step is to interpret it and build the corresponding HTML.

sequenceDiagram
    participant C as Browser
    participant S as Server
    C->>S: 1. HTTP request (GET /api/article/2/) and maybe auth
    S->>S: 2. Query the DB
    S->>S: 3. Build a JSON with the article data
    S-->>C: 4. Return JSON
    C->>C: 5. Parse the JSON
    C->>C: 6. Build the HTML with its rendering engine

With HTML over WebSockets, that same request travels over a permanent channel and the response is already assembled HTML, with no JSON in between. And since the channel never closes, the server can even get ahead and send changes without the client asking.

The flow with WebSockets is now the following, ignoring the initial connection and authentication, which happen only once when the channel opens:

sequenceDiagram
    participant C as Browser
    participant S as Server (Back-End)
    C->>S: 1. Sends a text: "I want /article/2/"
    S->>S: 2. Query the DB
    S->>S: 3. Render HTML with its template engine
    S-->>C: 4. Return the assembled HTML/CSS/JS
"
...
" C->>C: 5. Place the HTML where it belongs

Simple, elegant and fast. The client takes care of placing the HTML where it belongs and listening for events. The server handles the rest. You do not have to worry about client state or rendering logic, since everything lives in the Back-End.

The full, complex cycle, including opening the connection and authentication, would look like this:

sequenceDiagram
    participant C as Browser
    participant S as Server (Back-End)
    C->>S: 1. Opens WebSocket connection and authenticates
    Note over C,S: A single persistent channel
    C->>S: 2. Sends a text: "I want /article/2/"
    S->>S: 3. Query the DB
    S->>S: 4. Render HTML with its template engine
    S-->>C: 5. Return the assembled HTML/CSS/JS
"
...
" C->>C: 6. Place the HTML where it belongs Note over S,C: The server can also push
changes without the client asking (broadcast)

On top of that, by its very architecture, it carries intrinsic advantages over other solutions.

What are its advantages?

  • There is only one rendering engine, cutting down complexity.
  • You do not need to build an API: the server generates HTML and sends it to the client, with no middleman.
  • State lives on the server. It is not memoryless request-response: there is a process per connected client that remembers where it is. It is the opposite of htmx, which is deliberately stateless.
  • Direct connection to the database, with no JSON or GraphQL middleman.
  • Real real-time: clients receive changes as fast as possible, without polling the server.
  • Broadcast: the server can push changes to every connected client at once. Building a chat, a dashboard or a multiplayer game comes for free.
  • Less traffic and less latency per action: a single persistent connection avoids repeating the TCP handshake and the HTTP headers on every interaction. It is not that "the WebSocket protocol is magically faster" (HTTP/2 and HTTP/3 have narrowed that gap a lot in request-response), it is that you skip the round trip and send assembled HTML.
  • Build a SPA with barely any JavaScript, without heavy frameworks like React, Angular or Vue.
  • Reasonable SEO: since the HTML is rendered on the server, the first load is indexable. Careful though, a crawler does not see the updates that arrive later over the WebSocket, so the important content must be in that first response.
  • Safer against injection: since the server renders and escapes the HTML before sending it over the channel, an attempt to sneak in a <script> travels as inert text and reaches your neighbor's screen as plain letters, not as code. The same architecture that makes a chat trivial makes it immune to XSS.

What are its drawbacks?

  • The server needs more resources: it keeps a WebSocket open and, usually, each client's state in memory. Scaling horizontally forces you to share that state (in Django, with Channels + an ASGI server + Redis as the channel layer). That said, the real problem only shows up with a large number of simultaneous clients, and careful design can mitigate it. My site has handled peaks of 600 simultaneous readers without trouble, running on hardware similar to a Raspberry Pi 3 with other services running alongside.
  • Latency: with a lot of physical latency, the "instant" feel suffers.
  • It does not work offline. If the connection drops, the site stops working. You have to design the reconnection experience and fault tolerance.
  • The initial learning curve is steeper than dropping in a <script>: running a WebSocket server is not trivial, and you have to learn to handle the LiveView pattern.

The current landscape: which frameworks exist?

The hypermedia movement already has an implementation in almost every language. Look at the transport column: the ones running over WebSocket (the LiveView pattern, real-time and bidirectional) coexist with the HTTP and SSE cousins, for when you do not need that two-way channel. You can start here:

Language Framework Transport Server push? Status
Elixir Phoenix LiveView WebSocket Yes Mature (1.x, LiveView 1.0 in Dec 2024)
Ruby Hotwire (Turbo + Stimulus) HTTP + WebSocket/SSE (Streams) Yes Turbo 8 with morphing
Python / Django Django LiveView WebSocket Yes Active (mine)
Python / Django Reactor WebSocket Yes Active
Python / Django djust WebSocket Yes New, with a Rust VDOM
Python / Django django-unicorn HTTP / AJAX No Active
Python / Django Tetra AJAX + WebSocket Yes Young, on Alpine.js
C# / .NET Blazor (Interactive Server) WebSocket (SignalR) Yes .NET 9, with render modes
PHP / Laravel Livewire 3 + Reverb WebSocket Yes Reverb, Laravel's own WebSocket server (2024)
Agnostic (JS) htmx HTTP + WS/SSE extensions Yes (extension) 2.0
Agnostic (JS) Datastar SSE Yes 1.0

SSE, the cheap option

WebSockets is powerful, but keeping a bidirectional channel open per client has a cost. And often you do not need it: if the flow is mostly server to client (notifications, a live feed, a dashboard, the tokens of an AI response), Server-Sent Events (SSE) are enough. It is the same idea, sending ready-made HTML over the wire, but over a plain HTTP channel that only goes one way.

It is the cheap option: the simplest infrastructure. By not keeping a stateful process per client, it is easier to load-balance and scale.

However, it has limitations:

  • It is one-way. Only the server pushes. If the client wants to send something, there is no channel for it: it has to make a separate HTTP request.
  • Text only. It carries UTF-8, no binary (WebSocket does).
  • Worse for heavy bidirectional work. In a chat, collaborative editing or a game, that loose back-and-forth over HTTP weighs more than an always-open WebSocket.

htmx has an almost identical implementation in spirit with its SSE extension. You declare the channel with an attribute and the HTML that arrives in each event places itself:

<div hx-ext="sse" sse-connect="/updates" sse-swap="message">
    Real-time content appears here
</div>

Under the hood it uses the browser's own EventSource, with reconnection included, and the server sends HTML fragments over text/event-stream. The same philosophy as this article, just changing the transport. Along the same lines is Datastar, which unifies Alpine-style reactivity over SSE.

The quick rule: if you need bidirectional, low-latency communication (chat, collaboration, games), WebSocket; if you only push from the server, SSE is simpler and cheaper to operate.

Final notes

HTML over WebSockets is not the answer to everything, and none of these technologies is. The transport is dictated by your problem: if you need real-time back-and-forth (a chat, a live panel, something collaborative), WebSockets; if you only push from the server, SSE; if request-and-response is enough, htmx over HTTP.

Every project is its own world, with its own quirks and limits. If you take away one thing, let it be the idea underneath: send HTML instead of JSON, stay in a single language and cross the API, the contracts and half the Front-End off your list.

Trust a good architecture, not trendy frameworks or patterns.

Sources

  • Phoenix LiveView, official docs: the canonical pattern, with per-client state on the server and diffs sent over WebSocket.
  • Phoenix LiveView 1.0 released, Phoenix blog: the 1.0 milestone (December 2024), six years after the first commit.
  • Hotwire, official site: where the name "HTML Over The Wire" comes from and why Turbo runs mostly over HTTP.
  • htmx docs: hypermedia over HTTP, deliberately stateless, with WebSockets and SSE only through extensions.
  • Turbo Handbook: Page Refreshes, on Turbo 8's morphing to update only what changed and preserve scroll and focus.
  • idiomorph, the DOM morphing library that several of these solutions use.
  • Datastar, the hypermedia framework that bets on SSE instead of WebSockets.
  • Using server-sent events, MDN: how SSE works (EventSource, automatic reconnection, Last-Event-ID, text/event-stream).
  • Laravel Reverb, Laravel's first-party WebSocket server (2024): proof that PHP also does real-time without third-party extensions.
  • ASP.NET Core Blazor render modes, Microsoft Learn: the Interactive Server mode runs over SignalR (WebSockets).

This work is under a Attribution-NonCommercial-NoDerivatives 4.0 International license.

Help me keep writing

Every coffee gives me a push toward the next article.

Comments

There are no comments yet.

1 mention from another site

You may also like