HTML sobre WebSockets: SPAs en tiempo real sin apenas JavaScript
Construir una SPA (Single-page Application) es un puzzle complejo: un framework de JavaScript que dibuja la vista, una API sirviendo JSON, y 2 lógicas independientes obligadas a entenderse por medio de contratos. Es un escenario aceptado y profesionalizado. Pero que sea un estándar no implica que sea la única vía. Me gustaría darte a conocer otro enfoque, que no es nuevo pero sí ha ganado fuerza a lo largo de los años: HTML sobre WebSockets.
El punto es el siguiente: en lugar de mandar JSON y montar el HTML en el navegador, es el servidor quien manda el HTML ya hecho y el cliente solo lo coloca en su sitio. Toda la lógica de pintado se queda en el Back-End, en un solo lenguaje, y sin necesidad de crear contratos o una API. A este patrón se le conoce como hypermedia o HTML over the wire (HTML "por el cable"). La importancia de cómo viaja el HTML es que determina la latencia y la bidireccionalidad de la comunicación. Hay tres variantes:
- Por HTTP, petición a petición, como htmx o Unicorn.
- Por SSE, se abre un canal unidireccional y continuo del servidor al cliente, como Datastar.
- Por WebSockets, un canal permanente y bidireccional, como Phoenix LiveView o Django LiveView.
El canal es tan importante que determina la arquitectura de la aplicación y el patrón de comunicación.
En este artículo voy a hablarte de HTML sobre WebSockets: la variante en tiempo real y bidireccional de la familia. La que te permite crear un SPA sin apenas JavaScript, con un solo lenguaje, sin contratos y un solo motor de renderizado. Veremos qué es, cómo funciona y cuándo compensa frente a sus primos por HTTP o SSE.
Origen
Chris McCord, creador de Phoenix (el Framework más popular dentro del ecosistema Elixir), presentó en ElixirConf 2019 una tecnología llamada LiveView. En apenas 15 minutos creó un clon de Twitter que funcionaba en tiempo real sin necesidad de incorporar JavaScript renderizador o un framework popular (React, Angular, Vue...) que gestione la Vista, demostrando que era posible quedarse en el Back-End y lograr productividad con una dulce aroma a buen rendimiento. Desde entonces se ha popularizado esta solución, inspirando a otros desarrolladores para crear implementaciones de HTML sobre WebSockets en otros lenguajes. Se puede volver al Back-End pero sin renunciar a lo bueno del Front-End.
¿Cómo funciona?
Aunque en principio no lo parezca, sí que se usa JavaScript en el cliente. Su labor no es renderizar sino crear un canal de comunicación con WebSockets y situar el HTML recibido en el lugar adecuado. Además de otras tareas secundarias como animaciones, gestión de eventos, etc.
La solución de McCord es no enviar al Front-End un JSON, sino HTML que no necesite ser preprocesado. De ese modo trasladamos la carga del dibujado, y toda su lógica, al Back-End. Ya pero... ¿Cómo hacemos que el servidor nos envíe nuevo contenido de forma inmediata y sin realizar una petición? Sencillo: con WebSockets.
Repasemos el sistema tradicional de la introducción. Desde la Web hago una petición HTTP, el navegador inicia la acción, obteniendo en la respuesta un JSON con toda la información en crudo. Lo siguiente es interpretar y crear el HTML correspondiente.
sequenceDiagram
participant C as Navegador
participant S as Servidor
C->>S: 1. Petición HTTP (GET /api/articulo/2/) y tal vez se autentica
S->>S: 2. Consulta la BD
S->>S: 3. Genera un JSON con los datos del artículo
S-->>C: 4. Devuelve JSON
C->>C: 5. Interpreta el JSON
C->>C: 6. Construye el HTML con su motor de renderizado
Con HTML sobre WebSockets, esa misma petición viaja por un canal permanente y la respuesta ya es HTML montado, sin JSON de por medio. Y como el canal nunca se cierra, el servidor puede incluso adelantarse y mandar cambios sin que el cliente pregunte.
Ahora el flujo con WebSockets es el siguiente, ignorando la conexión inicial y la autenticación, que se hace una sola vez al abrir el canal:
sequenceDiagram
participant C as Navegador
participant S as Servidor (Back-End)
C->>S: 1. Envía un texto: "Quiero /articulo/2/"
S->>S: 2. Consulta la BD
S->>S: 3. Renderiza HTML con su motor de plantillas
S-->>C: 4. Devuelve el HTML/CSS/JS ya montado
"... "
C->>C: 5. Coloca el HTML en su sitio
Sencillo, elegante y rápido. El cliente se ocupa de colocar el HTML en su sitio y escuchar los eventos. Del resto se ocupa el servidor. No hay que preocuparse por el estado del cliente ni por la lógica de renderizado, ya que todo está en el Back-End.
El ciclo completo complejo, incluyendo la apertura de la conexión y la autenticación, sería así:
sequenceDiagram
participant C as Navegador
participant S as Servidor (Back-End)
C->>S: 1. Abre conexión WebSocket y se autentica
Note over C,S: Un único canal persistente
C->>S: 2. Envía un texto: "Quiero /articulo/2/"
S->>S: 3. Consulta la BD
S->>S: 4. Renderiza HTML con su motor de plantillas
S-->>C: 5. Devuelve el HTML/CSS/JS ya montado
"... "
C->>C: 6. Coloca el HTML en su sitio
Note over S,C: El servidor también puede empujar
cambios sin que el cliente pregunte (broadcast)
Pero además, por su arquitectura, guarda ventajas intrínsecas frente a otras soluciones.
¿Cuáles son sus ventajas?
- Solo hay un motor de renderizado, simplificando la complejidad.
- No necesitas crear una API: el servidor genera HTML y lo manda al cliente, sin intermediarios.
- El estado vive en el servidor. No es petición-respuesta sin memoria: hay un proceso por cada cliente conectado que recuerda en qué punto está. Es lo contrario a htmx, que es deliberadamente stateless.
- Conexión directa con la base de datos, sin intermediarios en JSON o GraphQL.
- Tiempo real de verdad, los clientes reciben los cambios tan rápido como sea posible, sin sondear al servidor.
- Broadcast: el servidor puede empujar cambios a todos los clientes conectados a la vez. Crear un chat, un dashboard o un juego multijugador sale gratis.
- Menos tráfico y menos latencia por acción: una sola conexión persistente evita repetir el saludo TCP y las cabeceras HTTP en cada interacción. No es que "el protocolo WebSocket sea mágicamente más rápido" (HTTP/2 y HTTP/3 han acortado mucho esa distancia en petición-respuesta), es que te ahorras el ida y vuelta y envías HTML ya montado.
- Crear un SPA sin apenas JavaScript ni frameworks pesados como React, Angular o Vue.
- SEO razonable: al renderizar HTML en el servidor, la primera carga es indexable. Ojo, las actualizaciones que llegan luego por el WebSocket no las ve un crawler, así que el contenido importante debe estar en esa primera respuesta.
- Más seguro frente a inyecciones: como el servidor renderiza y escapa el HTML antes de mandarlo por el canal, un intento de colar
<script>viaja como texto inerte y llega a la pantalla del vecino como unas letras, no como código. La misma arquitectura que lo hace trivial para un chat lo hace inmune a XSS.
¿Cuáles son sus inconvenientes?
- El servidor necesita más recursos: mantiene un WebSocket abierto y, normalmente, el estado de cada cliente en memoria. Escalar horizontalmente obliga a compartir ese estado (en Django, con Channels + un servidor ASGI + Redis como capa de canales). Aunque el problema real solo aparece con una gran cantidad de clientes simultáneos, y con un diseño cuidadoso se puede mitigar. Mi sitio ha experimentado picos de 600 lectores simultáneos sin problemas, y funciona en un equipo similar a una Raspberry Pi 3 con otros servicios corriendo en paralelo.
- Latencia: con mucha latencia física, la sensación de "instantáneo" se resiente.
- No funciona sin conexión. Si la conexión se cae, el sitio deja de funcionar. Hay que diseñar la experiencia de la reconexión y la tolerancia a fallos.
- La curva inicial es mayor que soltar un
<script>: tener un servidor de WebSockets no es trivial, y hay que aprender a manejar el patrón LiveView.
El panorama actual: ¿qué frameworks existen?
El movimiento hypermedia ya tiene implementación en casi todos los lenguajes. Fíjate en la columna de transporte: conviven las que van por WebSocket (el patrón LiveView, tiempo real y bidireccional) con los primos por HTTP y SSE, para cuando no necesitas ese canal en los dos sentidos. Puedes empezar por aquí:
| Lenguaje | Framework | Transporte | ¿Empuje del servidor? | Estado |
|---|---|---|---|---|
| Elixir | Phoenix LiveView | WebSocket | Sí | Maduro (1.x, LiveView 1.0 en dic. 2024) |
| Ruby | Hotwire (Turbo + Stimulus) | HTTP + WebSocket/SSE (Streams) | Sí | Turbo 8 con morphing |
| Python / Django | Django LiveView | WebSocket | Sí | Activo (el mío) |
| Python / Django | Reactor | WebSocket | Sí | Activo |
| Python / Django | djust | WebSocket | Sí | Nuevo, con VDOM en Rust |
| Python / Django | django-unicorn | HTTP / AJAX | No | Activo |
| Python / Django | Tetra | AJAX + WebSocket | Sí | Joven, sobre Alpine.js |
| C# / .NET | Blazor (Interactive Server) | WebSocket (SignalR) | Sí | .NET 9, con render modes |
| PHP / Laravel | Livewire 3 + Reverb | WebSocket | Sí | Reverb, servidor WebSocket propio de Laravel (2024) |
| Agnóstico (JS) | htmx | HTTP + extensiones WS/SSE | Sí (extensión) | 2.0 |
| Agnóstico (JS) | Datastar | SSE | Sí | 1.0 |
SSE, la solución barata
WebSockets es potente, pero mantener un canal bidireccional abierto por cada cliente cuesta. Y muchas veces no lo necesitas: si el flujo es sobre todo del servidor al cliente (notificaciones, un feed en vivo, un dashboard, los tokens de una respuesta de IA), te sobra con Server-Sent Events (SSE). Es la misma idea, mandar HTML ya hecho por el cable, pero por un canal de HTTP normal y corriente que solo va en un sentido.
Es la opción barata: la infraestructura más simple. Al no mantener un proceso con estado por cliente, es más fácil de balancear y escalar.
Sin embargo, tiene limitaciones:
- Es unidireccional. Solo el servidor empuja. Si el cliente quiere mandar algo, no hay canal para ello: tiene que hacer una petición HTTP aparte.
- Solo texto. Transporta UTF-8, nada de binario (WebSocket sí).
- Peor para lo bidireccional intenso. En un chat, una edición colaborativa o un juego, ese ida y vuelta por HTTP suelto pesa más que un WebSocket siempre abierto.
htmx tiene una implementación casi idéntica en espíritu con su extensión de SSE. Declaras el canal con un atributo y el HTML que llega en cada evento se coloca solo:
<div hx-ext="sse" sse-connect="/updates" sse-swap="message">
Aquí aparece el contenido en tiempo real
</div>
Por debajo usa la misma EventSource del navegador, con reconexión incluida, y el servidor manda fragmentos de HTML por text/event-stream. La misma filosofía de este artículo, cambiando el transporte. En esta línea está también Datastar, que unifica reactividad al estilo Alpine sobre SSE.
La regla rápida: si necesitas comunicación bidireccional y de baja latencia (chat, colaboración, juegos), WebSocket; si solo empujas desde el servidor, SSE es más simple y más barato de operar.
Apuntes finales
HTML sobre WebSockets no es la respuesta a todo, y ninguna de estas tecnologías lo es. El transporte lo dicta tu problema: si necesitas ida y vuelta en tiempo real (un chat, un panel en vivo, algo colaborativo), WebSockets; si solo empujas desde el servidor, SSE; si te basta con pedir y responder, htmx por HTTP.
Cada proyecto es un mundo, con sus particularidades y limitaciones. Si te tienes que quedar con algo, que sea la idea de debajo: mandar HTML en lugar de JSON, quedarte en un solo lenguaje y tachar de la lista la API, los contratos y medio Front-End.
Confía en una buena arquitectura, no en frameworks o patrones de moda.
Fuentes
- Phoenix LiveView, documentación oficial: el patrón canónico, con estado por cliente en el servidor y diffs enviados por WebSocket.
- Phoenix LiveView 1.0 released, blog de Phoenix: el hito 1.0 (diciembre de 2024), seis años después del primer commit.
- Hotwire, sitio oficial: de dónde viene el nombre "HTML Over The Wire" y por qué Turbo va sobre todo por HTTP.
- htmx docs: hypermedia por HTTP, deliberadamente stateless, con WebSockets y SSE solo por extensión.
- Turbo Handbook: Page Refreshes, sobre el morphing de Turbo 8 para actualizar solo lo que cambió y preservar scroll y foco.
- idiomorph, la librería de morphing del DOM que usan varias de estas soluciones.
- Datastar, el framework hypermedia que apuesta por SSE en lugar de WebSockets.
- Using server-sent events, MDN: el funcionamiento de SSE (
EventSource, reconexión automática,Last-Event-ID,text/event-stream). - Laravel Reverb, el servidor WebSocket de primera parte de Laravel (2024): la prueba de que PHP también hace tiempo real sin extensiones de terceros.
- ASP.NET Core Blazor render modes, Microsoft Learn: el modo Interactive Server corre sobre SignalR (WebSockets).
- Origen
- ¿Cómo funciona?
- ¿Cuáles son sus ventajas?
- ¿Cuáles son sus inconvenientes?
- El panorama actual: ¿qué frameworks existen?
- SSE, la solución barata
- Apuntes finales
- Fuentes
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.
Sure, it's on me!
Comments
There are no comments yet.