Les dues lliçons anteriors han resolt la integració d'un component Lit amb el món que l'envolta un cop ja està al navegador de l'usuari: HTML pla, o dins d'un altre framework d'aplicació. Aquesta lliçó planteja un problema de naturalesa diferent, que passa abans que el navegador tingui tan sols l'oportunitat d'executar una sola línia de JavaScript: què rep un usuari, o un motor de cerca, en el primer instant en què la resposta HTML d'una pàgina arriba des del servidor, abans que es carregui i s'executi el bundle de Lit? Per defecte, la resposta és "molt poc": una etiqueta <task-card> buida, sense contingut a dins, fins que el JavaScript es descarregui, s'executi i el component decideixi renderitzar-se pel seu compte. El renderitzat al servidor (Server-Side Rendering, SSR) existeix per canviar aquesta resposta.

Contingut

  1. El problema: una pàgina buida fins que carrega el JavaScript
  2. Què és la hidratació i per què importa
  3. Per què el SSR interessa especialment als Web Components
  4. Panorama de @lit-labs/ssr i el seu caràcter experimental
  5. La funció render de @lit-labs/ssr
  6. Declarative Shadow DOM: el mecanisme estàndard que ho fa possible
  7. Exemple conceptual: <task-card> renderitzada al servidor
  8. Limitacions actuals a tenir en compte

  1. El problema: una pàgina buida fins que carrega el JavaScript

Tot el desenvolupament de TaskFlow durant el curs ha assumit, sense dir-ho explícitament, que el navegador de l'usuari descarrega l'HTML de la pàgina, després descarrega i executa el JavaScript que defineix els components (customElements.define('task-card', TaskCard), i la resta), i només llavors cada etiqueta <task-card> present al marcat "cobra vida" i es renderitza amb el seu contingut real. Entre el primer instant (HTML rebut) i el segon (JavaScript executat i components definits), hi ha una finestra de temps —a vegades mil·lisegons, a vegades diversos segons en una connexió lenta o un dispositiu modest— en la qual la pàgina existeix, però els seus components Lit estan buits: el navegador coneix l'etiqueta <task-card> com un element sense comportament especial (el que l'especificació de Custom Elements anomena un element "no millorat", unupgraded), sense cap contingut visible dins del seu Shadow DOM, perquè aquest Shadow DOM ni tan sols existeix encara.

Per a una aplicació pensada per utilitzar-se després d'una interacció explícita de l'usuari (després d'iniciar sessió, per exemple), aquesta finestra sol ser acceptable. Per a contingut que ha d'estar visible al més aviat possible —una pàgina d'inici, un llistat de tasques que un usuari vol veure tan bon punt obre l'enllaç, o qualsevol pàgina que un motor de cerca hagi de poder indexar sense executar JavaScript—, aquesta finestra és exactament el problema que resol el SSR: generar, al servidor, l'HTML ja emplenat amb el contingut real de cada component, de manera que el navegador rebi des del primer instant una pàgina amb contingut visible, sense dependre de que el JavaScript s'hagi executat encara.

  1. Què és la hidratació i per què importa

Renderitzar al servidor només resol la meitat del problema: l'HTML que arriba al navegador ja té el contingut visible, però encara no és interactiu. Un botó dins d'una <task-card> renderitzada al servidor es veu en pantalla, però no respon a un clic fins que passa un segon pas, anomenat hidratació: el procés pel qual el JavaScript del component, un cop descarregat i executat al navegador, "pren possessió" de l'HTML ja existent —en lloc de destruir-lo i regenerar-lo des de zero— i el connecta amb la lògica reactiva real del component, inclosos els seus escoltadors d'esdeveniments i la seva capacitat de tornar-se a renderitzar davant de canvis futurs d'estat.

La hidratació és, en certa manera, el contrari de crear un component des de zero: en un renderitzat normal (sense SSR), Lit parteix d'una etiqueta buida i construeix tot el seu contingut intern la primera vegada que s'executa render(); en una hidratació, Lit troba contingut ja present al DOM (generat pel servidor) i necessita reconciliar-s'hi, reconeixent quines parts d'aquest HTML corresponen a quines expressions dinàmiques de la plantilla, sense tornar a construir res que ja estigui correctament present. Aquesta distinció importa perquè explica per què el SSR no és simplement "executar els mateixos components a Node.js en lloc del navegador": el navegador, després, ha de fer una feina addicional i diferent (hidratar) de la que fa quan no hi ha SSR de per mig (renderitzar des de zero).

  1. Per què el SSR interessa especialment als Web Components

El SSR no és una idea exclusiva de Lit; és una tècnica estesa a l'ecosistema de frameworks d'aplicació (Next.js per a React, Nuxt per a Vue, Angular Universal per a Angular), motivada històricament per dues raons que s'apliquen igual de bé a qualsevol component, inclosos els de Lit: el SEO (els motors de cerca indexen millor una pàgina que ja conté el contingut real a l'HTML, en lloc de dependre d'executar JavaScript per descobrir-lo, encara que els rastrejadors moderns han millorat força en aquest aspecte) i el rendiment percebut (l'usuari veu contingut significatiu abans, en lloc d'una pantalla buida o un indicador de càrrega durant la finestra descrita a l'apartat 1).

Els Web Components afegeixen, però, una dificultat pròpia que els frameworks d'aplicació tradicionals no tenen de la mateixa manera: el Shadow DOM. Un component de React o Vue, sense Web Components de per mig, renderitza directament al DOM normal de la pàgina, sense cap frontera d'encapsulació especial; l'HTML generat al servidor per a aquest component és, estructuralment, el mateix tipus d'HTML que qualsevol altre element de la pàgina. Un component Lit, en canvi, encapsula el seu contingut dins d'un Shadow DOM (com es va estudiar a la lliçó 04-01), i l'HTML servit des del servidor tradicional no té, per definició, forma de representar "aquí hi ha una arrel de Shadow DOM amb aquest contingut a dins" utilitzant només les etiquetes HTML de sempre. Sense una solució a aquest problema concret, el SSR per a Web Components seria, a la pràctica, impossible d'implementar de forma fidel al comportament real del component al navegador.

  1. Panorama de @lit-labs/ssr i el seu caràcter experimental

@lit-labs/ssr és el paquet que el propi equip de Lit manté per resoldre aquest problema: executar components Lit en un entorn de servidor (normalment Node.js) i produir HTML que inclogui, de forma fidel, el contingut real de cada component, inclòs el seu Shadow DOM. El prefix labs en el nom del paquet no és casual ni decoratiu: Lit reserva aquest espai de noms per a paquets que el propi equip considera encara en fase experimental, amb una API que pot canviar entre versions menors amb més llibertat que el nucli estable de lit utilitzat durant la resta del curs. Això no vol dir que @lit-labs/ssr sigui inutilitzable en producció —de fet, hi ha projectes reals que l'utilitzen—, però sí que convé abordar-lo amb l'expectativa de revisar el registre de canvis (changelog) amb més atenció de l'habitual abans d'actualitzar de versió, i d'acceptar que algunes peces de l'ecosistema al voltant (com la integració amb determinats frameworks de servidor) poden estar menys madures que el nucli de Lit.

El paquet s'instal·la de forma independent, igual que @lit/context al mòdul anterior:

npm install @lit-labs/ssr

I està pensat per executar-se al propi servidor de l'aplicació —dins d'un gestor de rutes d'Express, d'una funció de servidor d'un framework més ampli (com Next.js, amb adaptacions), o de qualsevol entorn Node.js capaç d'importar mòduls ES— no al navegador de l'usuari final.

  1. La funció render de @lit-labs/ssr

La peça central de @lit-labs/ssr és la seva pròpia funció render, diferent de la plantilla html habitual de Lit però pensada per consumir exactament el mateix tipus de valor que render() d'un component retornaria:

// servidor.js (fragmento conceptual, entorno Node.js)
import { render } from '@lit-labs/ssr';
import { html } from 'lit';
import './src/components/task-card.js';

async function generarHtmlDeTarjeta(tarea) {
  const resultado = render(html`
    <task-card
      titulo="${tarea.titulo}"
      estado="${tarea.estado}"
      prioridad="${tarea.prioridad}"
    ></task-card>
  `);

  let html_generado = '';
  for (const fragmento of resultado) {
    html_generado += fragmento;
  }
  return html_generado;
}

render(...) rep una plantilla html corrent —la mateixa sintaxi utilitzada durant tot el curs— i retorna un iterable (concretament, un generador) de fragments de cadena, no una única cadena completa de cop; això permet, en un servidor real, anar enviant la resposta al client per parts a mesura que es generen (streaming), en lloc d'esperar a tenir l'HTML complet en memòria abans de respondre, cosa especialment valuosa per a pàgines amb molts components o amb dades que tarden a resoldre's. L'exemple del fragment anterior concatena tots els fragments en una única cadena, per simplicitat, però un servidor real orientat a streaming escriuria cada fragment directament a la resposta HTTP tan bon punt estigués disponible.

Perquè això funcioni, <task-card> s'ha de registrar exactament igual que al navegador —el mateix customElements.define('task-card', TaskCard)—, amb la particularitat que @lit-labs/ssr proporciona les seves pròpies implementacions de les API del DOM que Lit necessita (HTMLElement, customElements, i la resta), ja que Node.js no les té de forma nativa; el paquet s'encarrega de simular aquest entorn prou com perquè la mateixa classe de component, sense cap modificació especial pensada per al servidor, pugui executar el seu cicle de renderitzat normal i produir un resultat correcte.

  1. Declarative Shadow DOM: el mecanisme estàndard que ho fa possible

La peça que resol el problema assenyalat a l'apartat 3 —com representar un Shadow DOM dins d'HTML pla, sense executar JavaScript— és Declarative Shadow DOM (DSD), una extensió relativament recent del propi estàndard de Shadow DOM, no una invenció pròpia de Lit ni de @lit-labs/ssr. DSD permet declarar, directament en HTML, una plantilla especial marcada amb l'atribut shadowrootmode, que el navegador reconeix i "adjunta" automàticament com el Shadow DOM real de l'element que la conté, sense necessitar cap crida a attachShadow(...) des de JavaScript:

<task-card>
  <template shadowrootmode="open">
    <style>/* estilos encapsulados del componente */</style>
    <article>
      <h3>Revisar propuesta de cliente</h3>
      <p>Estado: progreso · Prioridad: 3</p>
    </article>
  </template>
</task-card>

Quan el navegador processa aquest marcat, reconeix el <template shadowrootmode="open"> i el converteix, de forma nativa i sense executar ni una línia de JavaScript encara, en el Shadow DOM real de <task-card>; el contingut dins del <template> deixa de ser un simple <template> inert (com els <template> normals de l'HTML, que mai es mostren per si mateixos) i passa a ser l'arbre de Shadow DOM efectiu de l'element, visible en pantalla des del primer moment en què el navegador processa l'HTML, exactament igual que si attachShadow(...) s'hagués cridat des de JavaScript immediatament després de crear l'element. Aquest és, precisament, l'HTML que @lit-labs/ssr produeix quan renderitza un component amb Shadow DOM: no un simple <task-card>...</task-card> amb el contingut "aplanat" a dins, sinó aquesta estructura amb la plantilla de DSD anidada, fidel a com es comportaria el component al navegador.

És aquest suport natiu del navegador, no cap màgia interna de Lit, el que permet que la hidratació (apartat 2) funcioni amb fidelitat: quan el JavaScript de Lit s'executa després, troba ja un Shadow DOM real adjuntat a l'element (gràcies al DSD), amb el contingut correcte a dins, i només necessita reconnectar la lògica reactiva, en lloc d'haver de crear el Shadow DOM des de zero com faria sense SSR.

  1. Exemple conceptual: <task-card> renderitzada al servidor

Unint les peces anteriors, així es veuria, de forma conceptual, el flux complet per servir una pàgina inicial de TaskFlow amb diverses targetes ja renderitzades al servidor:

// servidor.js (conceptual, con un framework de servidor genérico)
import { render } from '@lit-labs/ssr';
import { html } from 'lit';
import './src/components/task-list.js';
import { cargarTareas } from './src/services/tareas-service.js';

app.get('/', async (peticion, respuesta) => {
  const tareas = await cargarTareas();

  const plantilla = html`
    <!DOCTYPE html>
    <html lang="es">
      <head><title>TaskFlow</title></head>
      <body>
        <task-list></task-list>
        <script type="module" src="/main.js"></script>
      </body>
    </html>
  `;

  respuesta.setHeader('Content-Type', 'text/html');
  for (const fragmento of render(plantilla)) {
    respuesta.write(fragmento);
  }
  respuesta.end();
});

El punt clau d'aquest flux és que cargarTareas() —la mateixa funció de servei introduïda a la lliçó 07-03 per simular una càrrega asíncrona— es resol abans de generar l'HTML, directament al servidor, amb await; a diferència de l'ús d'until vist en aquella lliçó (pensat per al navegador, on render() ha de mantenir-se síncron mentre la promesa es resol més endavant), el servidor pot esperar sense cap problema, perquè no hi ha cap restricció de sincronia equivalent a la de render() del costat del client: el servidor simplement no respon a la petició HTTP fins que la promesa de cargarTareas() es resol, i només llavors genera l'HTML, ja amb les tasques incloses dins del Shadow DOM de <task-list> i de cada <task-card> anidada. L'<script type="module" src="/main.js"> al final del <body> és el que dispara, ja al navegador, la hidratació: tan bon punt s'executa i defineix <task-list> i <task-card>, Lit reconeix el Shadow DOM ja present (gràcies al DSD) i el connecta amb la lògica reactiva real, sense tornar-lo a construir des de zero.

  1. Limitacions actuals a tenir en compte

@lit-labs/ssr resol el problema central del SSR per a Web Components, però convé conèixer, abans d'adoptar-lo en un projecte real, diverses limitacions vigents en el moment d'escriure aquest curs:

  • Suport de navegador de Declarative Shadow DOM: encara que DSD ja forma part de l'especificació estàndard i els navegadors principals el suporten, un projecte que necessiti donar suport a navegadors més antics ha de comprovar la compatibilitat exacta abans de dependre'n en producció, o incloure un polyfill per als casos sense suport natiu.
  • Cost de mantenir dos entorns d'execució: un component que s'executa tant al servidor (dins de l'entorn simulat de @lit-labs/ssr) com al navegador ha d'evitar dependre d'API exclusives del navegador (com window, document.querySelector sobre el document global, o temporitzadors del navegador) d'una forma que trenqui a l'entorn de servidor; codi com el ContadorTiempoRestanteController de la lliçó 06-03, que utilitza setInterval, necessitaria revisió acurada per comportar-se raonablement si s'intentés renderitzar al servidor sense adaptació.
  • Ecosistema d'integració amb frameworks de servidor encara en desenvolupament: la integració amb frameworks de servidor concrets (Express, o altres més específics de SSR d'aplicacions completes) varia en maduresa, i alguns fluxos —com el streaming incremental de fragments esmentat a l'apartat 5— requereixen més codi de connexió manual que l'equivalent ja resolt en frameworks d'aplicació més establerts, com Next.js per a React.
  • La hidratació no és automàtica ni gratuïta: encara que @lit-labs/ssr genera l'HTML inicial correctament, el propi procés d'hidratació al navegador (reconciliar el DOM ja existent amb la lògica reactiva) continua sent, en el moment d'escriure aquest curs, una peça de l'ecosistema que exigeix més atenció al detall que el renderitzat des de zero habitual, i no tots els patrons utilitzats lliurement durant el curs (per exemple, certes directives amb estat intern complex) estan garantits de comportar-se de forma idèntica en un component hidratat davant d'un renderitzat normalment des del principi.

Cap d'aquestes limitacions invalida el paquet; simplement assenyalen que, a diferència del nucli estable de Lit utilitzat durant la resta del curs, el SSR amb @lit-labs/ssr és un terreny on convé provar a fons el cas concret de l'aplicació abans de donar-lo per fet en producció.

Errors Habituals i Consells

  • Pensar que el SSR "aplana" el Shadow DOM en HTML normal: com s'ha explicat a l'apartat 6, @lit-labs/ssr no descarta el Shadow DOM en generar HTML; el representa fidelment mitjançant Declarative Shadow DOM, precisament perquè la hidratació posterior trobi una estructura real, no una aproximació simplificada.
  • Confondre el SSR amb la hidratació com si fossin el mateix: com s'ha explicat a l'apartat 2, són dos passos diferents i complementaris; el SSR genera l'HTML inicial al servidor, la hidratació connecta aquest HTML ja existent amb la lògica reactiva al navegador. Sense el segon pas, la pàgina tindria contingut visible però cap component respondria a interaccions.
  • Utilitzar API exclusives del navegador sense comprovar l'entorn: com s'ha assenyalat a l'apartat 8, un component pensat per executar-se també al servidor ha d'evitar assumir, sense comprovació, que window o document es comporten exactament igual que en un navegador real; l'entorn simulat de @lit-labs/ssr cobreix l'essencial, però no és un navegador complet.
  • Adoptar @lit-labs/ssr per a un projecte intern sense necessitat real de SEO ni de rendiment percebut crític: donat el seu caràcter experimental (apartat 4) i les seves limitacions (apartat 8), convé reservar-lo per als casos on el problema que resol —contingut visible abans que carregui el JavaScript— té un impacte real i mesurable, no adoptar-lo per sistema en qualsevol projecte Lit sense avaluar si el cost addicional està justificat.

Exercicis

  1. Explica, amb les teves pròpies paraules i basant-te en l'apartat 3, per què el SSR per a un component de React o Vue sense Web Components no necessita cap mecanisme equivalent a Declarative Shadow DOM, mentre que un component Lit sí que el necessita.
  2. Un company d'equip, després de llegir sobre @lit-labs/ssr, proposa utilitzar-lo per renderitzar al servidor el ContadorTiempoRestanteController de <task-card> (el temporitzador d'urgència de la lliçó 06-03) exactament tal com està escrit, esperant que l'HTML inicial ja mostri l'estat d'urgència correcte. Assenyala, basant-te en l'apartat 8, quin problema concret tindria aquest controlador en executar-se a l'entorn de servidor sense cap adaptació.
  3. Retorna a l'exemple de l'apartat 7 i explica per què await cargarTareas() és perfectament vàlid dins del gestor de la ruta del servidor, mentre que la lliçó 07-03 va insistir que render() d'un component Lit mai pot ser una funció async ni esperar directament una promesa. Són totes dues afirmacions compatibles, o una contradiu l'altra?

Solucions

  1. Un component de React o Vue sense Web Components no crea, en cap moment, una arrel de Shadow DOM separada; tot el seu contingut s'insereix directament al DOM normal de la pàgina, com qualsevol altre conjunt d'etiquetes HTML. L'HTML generat al servidor per a aquest component és, per tant, estructuralment indistingible de qualsevol altre fragment d'HTML de la pàgina, i no necessita cap mecanisme especial per representar-lo: n'hi ha prou amb les etiquetes normals, ja suportades per qualsevol motor HTML des de sempre. Un component Lit, en canvi, encapsula el seu contingut dins d'un Shadow DOM real (lliçó 04-01), una estructura que l'HTML tradicional no tenia forma d'expressar de manera declarativa fins a l'arribada del Declarative Shadow DOM; sense DSD, el servidor només podria generar el contingut "aplanat", sense cap frontera d'encapsulació, cosa que no reproduiria fidelment com es comporta realment el component al navegador.
  2. ContadorTiempoRestanteController utilitza setInterval (segons es va descriure a la lliçó 06-03) per recalcular periòdicament si una tasca està a punt de vèncer, actualitzant el seu estat en el temps real transcorregut al navegador. A l'entorn de servidor de @lit-labs/ssr, la petició HTTP es resol i respon en un instant concret, no roman "viva" indefinidament com una pestanya de navegador oberta; iniciar un setInterval durant aquest renderitzat no tindria cap sentit pràctic (el servidor no farà córrer aquest interval eternament per cada petició processada, i si ho fes, seria una fuita de recursos greu), i l'HTML generat només podria reflectir l'estat d'urgència calculat en l'instant exacte de la petició, no una actualització contínua. El controlador necessitaria, com a mínim, comprovar en quin entorn s'executa i evitar programar el setInterval si no hi ha un navegador real al darrere, deixant que sigui la hidratació posterior, ja al client, qui activi el temporitzador real.
  3. Totes dues afirmacions són perfectament compatibles, perquè es refereixen a restriccions de contextos diferents. La restricció de la lliçó 07-03 s'aplica al mètode render() d'un component Lit executant-se al navegador, on Lit necessita que aquest mètode retorni una plantilla de forma síncrona per poder actualitzar el DOM immediatament, sense bloquejar el fil principal del navegador esperant una promesa. El gestor d'una ruta de servidor, en canvi, no té aquesta restricció: és habitual i correcte que una funció de gestió de peticions HTTP sigui async i esperi amb await qualsevol operació asíncrona (una consulta a base de dades, una crida a un altre servei) abans de generar i enviar la resposta completa; el servidor, a diferència del render() d'un component, no necessita retornar alguna cosa "immediatament" en el mateix sentit, perquè la seva feina és respondre a una única petició HTTP, no mantenir una interfície d'usuari reactiva i fluida davant de canvis continus d'estat.

Conclusió

Aquesta lliçó ha presentat @lit-labs/ssr com la via, encara experimental però funcional, per generar al servidor l'HTML inicial de components Lit amb el seu Shadow DOM fidelment representat gràcies a Declarative Shadow DOM, resolent el problema de la pàgina buida fins que carrega el JavaScript, a costa d'un segon pas —la hidratació— i de diverses limitacions vigents que convé avaluar cas per cas abans d'adoptar-lo en producció. Amb la integració cap a HTML pla, cap a altres frameworks i cap al servidor ja coberta, queda un últim front d'integració amb el món exterior a TaskFlow, de naturalesa més pràctica: com empaquetar i publicar els propis components perquè altres projectes els puguin consumir, i què canvia si s'opta per escriure'ls en TypeScript en lloc del JavaScript utilitzat fins ara.

Curs de Lit

Mòdul 1: Introducció a Lit i Web Components

Mòdul 2: Plantilles Reactives i Renderitzat

Mòdul 3: Propietats i Estat Reactiu

Mòdul 4: Estils en Components Lit

Mòdul 5: Esdeveniments i Comunicació entre Components

Mòdul 6: Cicle de Vida i Comportament Avançat

Mòdul 7: Directives i Funcionalitats Avançades de Plantilles

Mòdul 8: Integració, Interoperabilitat i Desplegament

Mòdul 9: Proves i Bones Pràctiques

Mòdul 10: Projecte: Construint TaskFlow

© Copyright 2026. Tots els drets reservats