HTML sobre WebSockets

La forma tradicional de conseguir una SPA (Single-page Application) es dividir las responsabilidades, el Back-End sirve la información y el Front-End la dibuja dinámicamente. Tristemente implica un doble esfuerzo en el desarrollo siendo necesario la creación de 2 aplicaciones con tecnologías diferentes aumentando costes e involucrando a dos perfiles especializados. Aunque, por supuesto, es el precio que debemos pagar si queremos una web en tiempo real y que se renderice en un pestañeo. ¿O hay una alternativa? ¿Con incluso mejor rendimiento? Así es, y además es más fácil de desarrollar al trabajar con un solo lenguaje. Esta arquitectura se denomina: HTML sobre WebSockets, aunque también se conoce como HTML over the Wire o Hotwire.

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?

Disclaimer: ¡sí se usa JavaScript! 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: Petición HTTP (GET /api/articulo/2/)
    S-->>C: JSON en crudo
    C->>C: Interpreta el JSON y construye el HTML

Mientras que HTML sobre WebSockets puede ser el envío de un JSON donde se devuelve HTML/CSS/JS. O incluso puede quitar el propio envío quedando a la escucha.

Veamos el ejemplo donde se renderiza el artículo número 2 de un blog. Este es el ciclo completo, incluida la gran ventaja de que el servidor puede empujar cambios sin que nadie los pida:

sequenceDiagram
    participant C as Navegador
    participant S as Servidor (Back-End)
    C->>S: 1. Abre conexión WebSocket
    Note over C,S: Un único canal persistente
    C->>S: 2. "Quiero /articulo/2/"
    S->>S: 3. Consulta la BD y renderiza la plantilla
    S-->>C: 3. Devuelve el HTML/CSS/JS ya montado
    C->>C: 4. Coloca el HTML en su sitio
    Note over S,C: El servidor también puede empujar
cambios sin que el cliente pregunte (broadcast)

1. Conectamos

Partimos con una conexión. Ya hay un tubo de comunicación entre cliente y servidor.

2. Petición de componente

El cliente pide el contenido de la ruta "/articulo/2/" a través del canal.

3. Recepción de HTML/CSS/JS

El servidor genera el HTML/CSS/JS, usando el sistema de plantillas del Back-End, y lo devuelve por el canal.

4. Impresión

Por último, el Front-End lo sitúa en el lugar adecuado o asignado.

Mi solución: Django LiveView

En Python me lié la manta a la cabeza y creé Django LiveView, un framework para construir SPAs en tiempo real escribiendo solo Python, inspirado directamente en Phoenix LiveView.

La idea es la que llevamos viendo: un único WebSocket siempre abierto, el servidor renderiza HTML con las plantillas de Django y lo devuelve por el canal. No hay API REST en medio, no hay JSON que serializar, no hay un segundo proyecto en JavaScript que mantener. Tú escribes atributos HTML y decoradores de Python, y el estado vive en el servidor.

¿Por qué otro framework más? Porque cuando comparo los principales de Django con un mismo caso real, los basados en WebSockets ganan por goleada. En mi benchmark contra Phoenix LiveView y en la comparativa entre soluciones de Django, la latencia por acción queda así:

  • Django LiveView: 9,35 ms (WebSocket)
  • Reactor: 12,00 ms (WebSocket)
  • django-htmx: 16,48 ms (HTTP/AJAX)
  • Unicorn: 26,76 ms (HTTP/AJAX)
  • SSR clásico: 47,25 ms (HTTP)

Las dos soluciones sobre WebSockets no solo son las más rápidas, es que hacen cero peticiones HTTP por acción. Si vienes de htmx y te preguntas si el salto merece la pena, lo cuento en detalle en De htmx a Django LiveView: no es que uno sea mejor que otro, es que resuelven cosas distintas. Y por si te queda alguna duda de hasta dónde llega la técnica, con ella he llegado a montar un motor de videojuegos para Django.

¿Dónde puedo ver una demostración?

He creado un prototipo en Django de un Blog con 100 entradas, cada artículo esta relacionado con sus respectivos comentarios. Además existe un apartado para visualizar el artículo completo, un paginador y una sección estática con algunos párrafos.

Aquí puedes ver como los cambios son reflejados en todos los clientes.

Si observáis la url, nunca se cambia de página, y... ¡aun así funciona! ¿Quieres probarlo por ti mismo? Tienes la posibilidad de levantarlo a partir del código fuente, esta Dockerizado a un comando de distancia para arrancarlo. Si buscas algo listo para producción, mejor empieza directamente por Django LiveView.

¿Cuáles son sus ventajas?

  • Solo hay un motor de renderizado, simplificando la complejidad.
  • Conexión directa con la base de datos, evitando la creación de una API.
  • 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. Un chat, un dashboard o un juego multijugador salen casi 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. Mis mediciones lo respaldan: el benchmark da hasta 5 veces menos latencia que un SSR clásico.
  • 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.

¿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).
  • Latencia y reconexiones: si la conexión se cae, hay que recuperarla con elegancia. Y con mucha latencia física, la sensación de "instantáneo" se resiente frente a algo optimista en el cliente.
  • No funciona sin conexión. Al vivir la lógica en el servidor, si no hay red, no hay interfaz. Para experiencias offline-first sigue haciendo falta JavaScript en el cliente.
  • La curva inicial es mayor que soltar un <script>: hay que configurar ASGI, la capa de canales y desplegar algo más que un WSGI de toda la vida.

El panorama actual: ¿qué frameworks existen?

Cada lenguaje tiene ya su clon de LiveView, y varios han alcanzado la versión estable. Puedes empezar por aquí:

Lenguaje Framework Transporte ¿Empuje del servidor? Estado
Elixir Phoenix LiveView WebSocket Maduro (1.x, LiveView 1.0 en dic. 2024)
Ruby Hotwire (Turbo + Stimulus) WebSocket / SSE (Action Cable) Turbo 8 con morphing
Python / Django Django LiveView WebSocket Activo (el mío)
Python / Django Reactor WebSocket Activo
Python / Django djust WebSocket Nuevo, con VDOM en Rust
Python / Django django-unicorn HTTP / AJAX No Activo
Python / Django Tetra AJAX + WebSocket Joven, sobre Alpine.js
Python / Django Sockpuppet WebSocket Menos activo
C# / .NET Blazor (Interactive Server) WebSocket (SignalR) .NET 9, con render modes
PHP / Laravel Livewire 3 + Reverb WebSocket Reverb estable desde 2024
Agnóstico (JS) htmx HTTP + extensiones WS/SSE Sí (extensión) 2.0
Agnóstico (JS) Datastar SSE 1.0

Y sí, PHP también juega: desde 2024 Laravel tiene su propio servidor WebSocket de primera parte, Reverb, que junto a Livewire 3 y Echo hace tiempo real sin trucos. Ya no hace falta tirar de extensiones raras.

¿Y si no necesito ir en los dos sentidos? SSE

No siempre hace falta un WebSocket. Si el flujo es sobre todo del servidor al cliente (notificaciones, un feed, un dashboard, tokens de una IA), los Server-Sent Events (SSE) son una alternativa estupenda. Son HTTP puro, se reconectan solos, atraviesan proxies sin dramas y con HTTP/2 se acabó el límite de conexiones paralelas.

Es la apuesta de Datastar, que unifica reactividad al estilo Alpine con SSE, y una de las extensiones estrella de htmx. La regla rápida: si necesitas comunicación bidireccional de baja latencia (chat, edición colaborativa, juegos), WebSocket; si solo empujas desde el servidor, SSE suele ser más simple y barato de operar. Ambos sirven para lo mismo que cuenta este artículo: mandar HTML ya hecho por el cable.

Apuntes finales

El movimiento hypermedia se ha ganado su hueco: htmx se hizo enorme, Phoenix LiveView llegó a la 1.0, Rails abraza Hotwire por defecto, .NET tiene Blazor y Laravel su Reverb. Volver al Back-End dejó de ser nostalgia para convertirse en una decisión de arquitectura perfectamente razonable.

No es la solución definitiva, pero es un placer no correr la maratón del Front-End para no quedarte desactualizado, y poder centrarte en el lenguaje de servidor.

En serio, ¿qué puedes perder por probarlo?

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.

You may also like