HTML via WebSockets: real-time SPA's met nauwelijks JavaScript

Het idee is als volgt: in plaats van JSON te versturen en de HTML in de browser samen te stellen, stuurt de server de reeds gebouwde HTML en plaatst de client deze simpelweg op de juiste plek. Alle rendering-logica blijft in het back-end, in één enkele taal, zonder dat er contracten of een API nodig zijn. Dit patroon staat bekend als hypermedia of HTML over the wire. Wat belangrijk is bij hoe de HTML wordt verzonden, is dat dit de latentie en de bidirectionaliteit van de communicatie bepaalt. Er zijn drie varianten:

  • Via HTTP: per verzoek, zoals htmx of Unicorn.
  • Via SSE (Server-Sent Events): het openen van een eenrichtingskanaal van server naar client, zoals Datastar.
  • Via WebSockets: een permanent, bidirectioneel kanaal, zoals Phoenix LiveView of Django LiveView.

Het kanaal is zo cruciaal dat het de architectuur van de applicatie en het communicatiepatroon bepaalt. In dit artikel bespreek ik HTML via WebSockets: de real-time en bidirectionele variant van de familie. De variant waarmee je een SPA kunt bouwen met nauwelijks JavaScript, in één taal, zonder contracten en met een enkele rendering-engine. We zullen kijken wat het is, hoe het werkt en wanneer het voordeliger is vergeleken met de HTTP- of SSE-varianten.

Oorsprong

Chris McCord, maker van Phoenix (het populairste framework in het Elixir-ecosysteem), presenteerde tijdens ElixirConf 2019 een technologie genaamd LiveView. In slechts 15 minuten bouwde hij een Twitter-kloon die in real-time werkte zonder rendering-JavaScript of een populair framework (zoals React, Angular, Vue...) toe te voegen om de View te beheren. Hiermee bewees hij dat je in het back-end kon blijven en toch productief kon zijn met uitstekende prestaties. Sindsdien is de oplossing populair geworden en heeft het andere ontwikkelaars geïnspireerd om HTML-via-WebSockets-implementaties in andere talen te bouwen. Je kunt terugkeren naar het back-end zonder de goede aspecten van het front-end op te geven.

Hoe werkt het?

Hoewel het misschien niet zo lijkt, wordt er aan de client-zijde nog steeds JavaScript gebruikt. De taak hiervan is echter niet het renderen, maar het creëren van een communicatiekanaal via WebSockets en het plaatsen van de ontvangen HTML op de juiste plek. Daarnaast voert het secundaire taken uit zoals animaties en event-handling.

De oplossing van McCord is om geen JSON naar het front-end te sturen, maar HTML die geen voorbewerking nodig heeft. Op die manier verplaatsen we de renderbelasting en alle bijbehorende logica naar het back-end. Maar hoe zorgen we ervoor dat de server ons onmiddellijk nieuwe inhoud stuurt zonder dat er een verzoek wordt gedaan? Eenvoudig: via WebSockets.

Laten we het traditionele systeem uit de inleiding herhalen. Vanaf de webpagina maak ik een HTTP-verzoek; de browser start de actie en ontvangt als antwoord een JSON met alle ruwe informatie. De volgende stap is om dit te interpreteren en de bijbehorende HTML te bouwen.

Het traditionele proces:

  1. Browser → Server: HTTP-verzoek (bijv. GET /api/article/2/) en eventueel authenticatie.
  2. Server: Query uitvoeren in de database.
  3. Server: Een JSON bouwen met de artikelgegevens.
  4. Server → Browser: JSON retourneren.
  5. Browser: De JSON parsen.
  6. Browser: De HTML bouwen met de rendering-engine.

Bij HTML via WebSockets reist datzelfde verzoek over een permanent kanaal en is het antwoord al samengestelde HTML, zonder tussenkomst van JSON. En omdat het kanaal nooit sluit, kan de server zelfs proactief wijzigingen sturen zonder dat de client hierom vraagt.

De flow met WebSockets (na initiële verbinding en authenticatie):

  1. Browser → Server: Verstuurt een tekst: "Ik wil /article/2/".
  2. Server: Query uitvoeren in de database.
  3. Server: HTML renderen met de template-engine.
  4. Server → Browser: De samengestelde HTML/CSS/JS retourneren.
  5. Browser: De HTML op de juiste plek plaatsen.

Eenvoudig, elegant en snel. De client houdt zich bezig met het plaatsen van de HTML en het luisteren naar events; de server regelt de rest. Je hoeft je geen zorgen te maken over de status van de client of rendering-logica, aangezien alles in het back-end leeft.

De volledige cyclus (inclusief verbinding):

  1. Browser → Server: Opent WebSocket-verbinding en authenticeert (één persistent kanaal).
  2. Browser → Server: Verstuurt een tekst: "Ik wil /article/2/".
  3. Server: Query uitvoeren in de database.
  4. Server: HTML renderen met de template-engine.
  5. Server → Browser: De samengestelde HTML/CSS/JS retourneren.
  6. Browser: De HTML op de juiste plek plaatsen.

Opmerking: De server kan ook wijzigingen pushen zonder dat de client hierom vraagt (broadcast).

Wat zijn de voordelen?

Door deze architectuur ontstaan er intrinsieke voordelen ten opzichte van andere oplossingen:

  • Eén rendering-engine: Dit vermindert de complexiteit aanzienlijk.
  • Geen API nodig: De server genereert HTML en stuurt deze direct naar de client, zonder tussenpersoon.
  • State leeft op de server: Het is geen geheugenloze request-response cyclus; er is een proces per verbonden client dat onthoudt waar deze zich bevindt. Dit is het tegenovergestelde van htmx, dat bewust stateless is.
  • Directe databaseverbinding: Er is geen JSON- of GraphQL-tussenlaag nodig.
  • Echte real-time communicatie: Clients ontvangen wijzigingen zo snel mogelijk, zonder dat ze de server hoeven te pollen.
  • Broadcast: De server kan wijzigingen in één keer naar alle verbonden clients pushen. Het bouwen van een chat, een dashboard of een multiplayer game is hiermee vrijwel gratis.
  • Minder verkeer en lagere latentie per actie: Eén enkele persistente verbinding voorkomt het herhalen van de TCP-handshake en HTTP-headers bij elke interactie. Het gaat er niet om dat "het WebSocket-protocol magisch sneller is", maar dat je de round-trip overslaat en direct samengestelde HTML verstuurt.
  • SPA bouwen met minimaal JavaScript: Zonder zware frameworks zoals React, Angular of Vue.
  • Redelijke SEO: Aangezien de HTML op de server wordt gerenderd, is de eerste lading indexeerbaar. Let wel op: een crawler ziet de updates die later via de WebSocket binnenkomen niet, dus de belangrijke inhoud moet in het eerste antwoord zitten.
  • Veiliger tegen injecties: Omdat de server de HTML rendert en ontsnapt (escapes) voordat deze over het kanaal wordt verzonden, reist een poging om een <script> in te smokkelen als inerte tekst. De architectuur die een chat triviaal maakt, maakt hem ook immuun voor XSS.

Wat zijn de nadelen?

  • Hogere serverbronnen: De server moet een WebSocket openhouden en meestal de status van elke client in het geheugen bewaren. Horizontaal schalen dwingt je om die status te delen (bijvoorbeeld in Django via Channels + een ASGI-server + Redis als channel layer). Dit is pas echt een probleem bij een zeer groot aantal gelijktijdige clients.
  • Latentie: Bij veel fysieke latentie lijdt het "instant" gevoel van de applicatie.
  • Werkt niet offline: Als de verbinding wegvalt, stopt de site met werken. Je moet de herverbindingservaring en fouttolerantie zorgvuldig ontwerpen.
  • Steilere initiële leercurve: Het draaien van een WebSocket-server is niet triviaal, en je moet leren omgaan met het LiveView-patroon.

Het huidige landschap: welke frameworks bestaan er?

De hypermedia-beweging heeft inmiddels implementaties in bijna elke taal. De volgende tabel toont de varianten die over WebSockets draaien (het LiveView-patroon) naast hun HTTP- en SSE-neven.

TaalFrameworkTransportServer push?Status
ElixirPhoenix LiveViewWebSocketJaMature (1.x, LiveView 1.0 in dec 2024)
RubyHotwire (Turbo + Stimulus)HTTP + WebSocket/SSE (Streams)JaTurbo 8 met morphing
Python / DjangoDjango LiveViewWebSocketJaActief
Python / DjangoReactorWebSocketJaActief
Python / DjangodjustWebSocketJaNieuw, met een Rust VDOM
Python / Djangodjango-unicornHTTP / AJAXNeeActief
Python / DjangoTetraAJAX + WebSocketJaJong, gebaseerd op Alpine.js
C# / .NETBlazor (Interactive Server)WebSocket (SignalR)Ja.NET 9, met render modes
PHP / LaravelLivewire 3 + ReverbWebSocketJaReverb, eigen WS-server van Laravel (2024)
Agnostisch (JS)htmxHTTP + WS/SSE extensiesJa (extensie)2.0
Agnostisch (JS)DatastarSSEJa1.0

SSE: de goedkopere optie

WebSockets zijn krachtig, maar het openhouden van een bidirectioneel kanaal per client heeft een kostprijs. Vaak is dit niet nodig; als de flow voornamelijk van server naar client gaat (notificaties, een live feed, een dashboard), dan volstaan Server-Sent Events (SSE). Hetzelfde idee—het versturen van kant-en-klare HTML over de lijn—maar via een eenvoudig HTTP-kanaal dat slechts één kant op gaat.

Het is de goedkopere optie met de simpelste infrastructuur. Omdat er geen stateful proces per client wordt bijgehouden, is het gemakkelijker om te load-balancen en te schalen. Er zijn echter beperkingen:

  • Eenrichtingsverkeer: Alleen de server pusht. Als de client iets wil sturen, moet dit via een apart HTTP-verzoek.
  • Alleen tekst: Het ondersteunt UTF-8, geen binaire data (wat WebSockets wel kunnen).
  • Minder geschikt voor zwaar bidirectioneel werk: In een chat, collaboratieve editor of game weegt het heen-en-weer gaan via HTTP zwaarder dan een altijd open WebSocket.

htmx heeft een spiritueel identieke implementatie met zijn SSE-extensie:

<div hx-ext="sse" sse-connect="/updates" sse-swap="message">
  Real-time inhoud verschijnt hier
</div>

Onder de motorkap gebruikt dit de EventSource van de browser en verstuurt de server HTML-fragmenten via text/event-stream. In dezelfde lijn ligt Datastar, dat Alpine-stijl reactiviteit unificeert over SSE.

De vuistregel: Heb je bidirectionele communicatie met lage latentie nodig (chat, samenwerking, games)? Kies WebSocket. Wil je alleen vanuit de server pushen? Dan is SSE eenvoudiger en goedkoper in beheer.

Slotopmerkingen

HTML via WebSockets is niet het antwoord op alles, net zoals geen enkele van deze technologieën dat is. Het transport wordt bepaald door je probleem:

  • Real-time heen-en-weer: WebSockets.
  • Alleen pushen vanaf de server: SSE.
  • Request-and-response is voldoende: htmx via HTTP.

Elk project is een wereld op zich met eigen eigenaardigheden en limieten. Als je één ding onthoudt, laat het dan dit zijn: verstuur HTML in plaats van JSON, blijf in één taal en schrap de API, de contracten en de helft van het front-end van je lijst. Vertrouw op een goede architectuur, niet op trendy frameworks of patronen.

Bronnen

  • Phoenix LiveView, official docs: Het canonieke patroon met per-client state op de server en diffs via WebSocket.
  • Phoenix LiveView 1.0 released, Phoenix blog: De mijlpaal van versie 1.0 (december 2024).
  • Hotwire, official site: Waar de naam "HTML Over The Wire" vandaan komt en waarom Turbo voornamelijk over HTTP draait.
  • htmx docs: Hypermedia over HTTP, bewust stateless, met WebSockets en SSE via extensies.
  • Turbo Handbook: Page Refreshes: Over de morphing in Turbo 8 om alleen wijzigingen bij te werken en scroll/focus te behouden.
  • idiomorph: De DOM-morphing bibliotheek die door diverse van deze oplossingen wordt gebruikt.
  • Datastar: Het hypermedia-framework dat inzet op SSE in plaats van WebSockets.
  • Using server-sent events, MDN: Hoe SSE werkt (EventSource, automatische herverbinding, Last-Event-ID, text/event-stream).
  • Laravel Reverb: De first-party WebSocket-server van Laravel (2024).
  • ASP.NET Core Blazor render modes, Microsoft Learn: De Interactive Server mode die via SignalR (WebSockets) draait.