En el módulo anterior escribiste varias plantillas con la función html, pero siempre con contenido completamente fijo: el mismo título, el mismo estado, la misma persona asignada en cada renderizado. Ha llegado el momento de entender qué es realmente esa función html y, sobre todo, qué ocurre por dentro cuando su contenido deja de ser estático. Esta lección abre el módulo 2 explicando el mecanismo interno del motor de plantillas de Lit: cómo se analiza una plantilla, cómo se actualiza el DOM cuando cambian los datos, y por qué este enfoque es mucho más eficiente que las alternativas clásicas de manipular el DOM a mano o reconstruirlo con innerHTML.
Contenido
htmlno es una plantilla de texto: es una función- Tagged template literals: la base de
html - Cómo Lit sabe qué partes son dinámicas
- Actualizar el DOM sin un Virtual DOM
- Por qué esto es distinto (y mejor) que
innerHTML - Primera interpolación en
<task-card> - Importante: esto todavía no es reactividad completa
html no es una plantilla de texto: es una función
html no es una plantilla de texto: es una funciónHasta ahora, cada vez que has escrito algo como esto:
render() {
return html`
<article>
<h3>Preparar la demo del sprint</h3>
<p>Estado: En curso</p>
</article>
`;
}es fácil pensar en html\...`como si fuera simplemente una cadena de texto con HTML dentro, algo parecido a escribir". Esa intuición es razonable a primera vista, pero es incorrecta y esconde justo el mecanismo que hace que Lit sea rápido y práctico de usar. html` es, en realidad, una función de JavaScript, y lo que se le pasa entre los acentos graves no es una cadena normal, sino algo llamado tagged template literal ("plantilla de literal etiquetada"), una característica que forma parte del propio estándar de JavaScript desde hace años, sin ninguna dependencia de Lit.
Cuando el motor de JavaScript encuentra html\
Hola
`, no concatena texto y llama a htmlcon el resultado. En su lugar, invoca a la funciónhtmlcon información estructurada sobre la plantilla: por un lado, un array con los trozos de texto literal ("estáticos"), y por otro, los valores que se han interpolado dentro de${}` (si los hay). Esta distinción —trozos fijos por un lado, valores dinámicos por otro— es la pieza central de todo lo que hace especial al motor de plantillas de Lit.
- Tagged template literals: la base de
html
htmlPara entender bien qué hace html, es útil ver primero cómo funciona un tagged template literal con una función de ejemplo, sin Lit de por medio:
function miEtiqueta(trozos, ...valores) {
console.log(trozos); // ["Hola ", ", tienes ", " tareas"]
console.log(valores); // ["Ana", 3]
}
const nombre = 'Ana';
const numeroDeTareas = 3;
miEtiqueta`Hola ${nombre}, tienes ${numeroDeTareas} tareas`;Al ejecutar este código, trozos recibe un array con los tres fragmentos de texto que quedan entre las interpolaciones ("Hola ", ", tienes " y " tareas"), mientras que valores recibe, en un array aparte, los dos valores interpolados ("Ana" y 3). Esta separación es exactamente lo que hace posible que una función etiquetada "sepa" qué partes de la plantilla son literales (nunca cambian) y cuáles son expresiones (pueden cambiar).
La función html de Lit es, en esencia, una función etiquetada de este mismo tipo, especializada en tratar el primer array como HTML y el segundo como datos que hay que insertar en puntos concretos de ese HTML. La diferencia respecto al ejemplo de miEtiqueta es que html no se limita a mostrar esos valores por consola: construye con ellos un objeto interno, llamado TemplateResult, que describe la plantilla de forma que Lit pueda procesarla de manera eficiente.
- Cómo Lit sabe qué partes son dinámicas
Aquí está la clave de todo el mecanismo. Como los trozos de texto literal siempre llegan por separado de los valores interpolados, Lit puede, la primera vez que ve una plantilla concreta, analizar únicamente su "esqueleto" fijo (los trozos de texto) y averiguar exactamente en qué posiciones del HTML van a aparecer valores dinámicos. Ese análisis se hace una sola vez por plantilla, no en cada renderizado.
El proceso, simplificado, es el siguiente:
- La primera vez que Lit encuentra una determinada plantilla
html\...`(identificada internamente por sus trozos de texto fijos), construye un elemento` real de HTML con marcadores especiales en los huecos donde había interpolaciones. - Lit recorre ese
<template>y anota con precisión dónde están esos huecos: pueden ser el contenido de un nodo de texto, el valor de un atributo, o directamente un nodo entero. - Este análisis se guarda en caché, asociado a esa plantilla concreta. Si el mismo componente vuelve a renderizar con la misma estructura de plantilla (cambiando solo los valores interpolados), Lit no repite el análisis: reutiliza directamente el mapa de huecos ya calculado.
- En cada renderizado posterior, Lit solo necesita clonar el
<template>(una operación muy barata en el navegador) y colocar los nuevos valores exactamente en los huecos ya identificados, sin volver a interpretar el HTML completo.
Esta idea —separar de una vez el "esqueleto" fijo de los "huecos" variables— es la razón por la que Lit puede permitirse actualizaciones extremadamente rápidas: en un renderizado normal, no se vuelve a parsear ni una sola etiqueta HTML; solo se comparan los valores nuevos con los anteriores en cada hueco y, si han cambiado, se actualiza justo ese punto del DOM real.
- Actualizar el DOM sin un Virtual DOM
Si has usado otras bibliotecas de interfaces (React, Vue...) es posible que conozcas el concepto de "Virtual DOM": una representación en memoria, como un árbol de objetos JavaScript, de cómo debería verse la interfaz. En cada actualización, esas bibliotecas generan un nuevo árbol virtual completo y lo comparan (mediante un algoritmo de "diffing") con el árbol virtual anterior, para calcular qué cambios mínimos hay que aplicar al DOM real.
Lit sigue un enfoque distinto, más directo, precisamente gracias al análisis descrito en el apartado anterior:
| Aspecto | Enfoque con Virtual DOM | Enfoque de Lit |
|---|---|---|
| Qué se genera en cada renderizado | Un árbol completo de objetos que representa toda la interfaz | Únicamente los valores nuevos a insertar en los huecos ya identificados |
| Cómo se detectan los cambios | Comparando (diffing) el árbol nuevo contra el árbol anterior, nodo a nodo |
Comparando directamente el valor nuevo de cada hueco contra el valor anterior de ese mismo hueco |
| Coste de un renderizado sin cambios reales | Se genera igualmente el árbol nuevo completo, aunque el resultado del diffing sea "no tocar nada" | Se evalúan los huecos, no se reconstruye ninguna estructura, y no se toca el DOM si los valores no han cambiado |
| Dónde vive el conocimiento de la estructura | En el árbol virtual, recalculado en cada render | En la plantilla <template> ya analizada y cacheada desde el primer renderizado |
En otras palabras: Lit no necesita "adivinar" qué ha cambiado comparando dos árboles completos, porque ya sabe, desde el primer análisis de la plantilla, exactamente dónde están los puntos que pueden cambiar. Solo tiene que revisar esos puntos concretos. Esto no significa que un enfoque sea "malo" y el otro "bueno" en términos absolutos —son estrategias distintas con sus propias ventajas—, pero sí explica por qué Lit puede ser una biblioteca tan ligera: no necesita implementar ni transportar en su código un algoritmo de diffing de árboles completo.
- Por qué esto es distinto (y mejor) que
innerHTML
innerHTMLOtra alternativa habitual, especialmente en código sin ninguna biblioteca, es reconstruir el HTML como una cadena de texto y asignarla mediante innerHTML:
// Enfoque manual con innerHTML, SIN Lit (solo con fines comparativos)
function actualizarTarjeta(contenedor, titulo, estado) {
contenedor.innerHTML = `
<article>
<h3>${titulo}</h3>
<p>Estado: ${estado}</p>
</article>
`;
}Este código "funciona", en el sentido de que muestra el HTML correcto, pero tiene problemas serios que el motor de plantillas de Lit evita por diseño:
- Destruye y recrea todo el árbol DOM en cada actualización: al asignar
innerHTML, el navegador descarta los nodos existentes dentro del contenedor y crea nodos completamente nuevos a partir del texto. Esto es costoso en términos de rendimiento y, además, hace que se pierda cualquier estado interno de esos nodos (por ejemplo, el foco de un campo de formulario, o el texto que un usuario estuviera seleccionando). - Vuelve a parsear el HTML entero en cada llamada: el navegador tiene que interpretar de nuevo toda la cadena de texto como HTML cada vez, sin ningún tipo de caché ni reutilización de análisis previos.
- Abre la puerta a vulnerabilidades de inyección de HTML: si
titulooestadocontuvieran contenido no confiable (por ejemplo, texto escrito libremente por un usuario con etiquetas<script>dentro),innerHTMLlo interpretaría como HTML real, ejecutándolo potencialmente. Lit, en cambio, inserta los valores interpolados como texto seguro por defecto, sin interpretarlos como HTML (salvo que se use explícitamente un mecanismo pensado para ese caso, que se verá más adelante en el curso).
Lit, gracias a la caché de plantillas del apartado 3, actualiza únicamente los nodos de texto o atributos concretos que han cambiado, conservando intacto el resto del árbol DOM. Esta es una diferencia fundamental de comportamiento, no solo de rendimiento.
- Primera interpolación en
<task-card>
<task-card>Con esta base ya se puede dar el primer paso práctico del módulo: interpolar un valor dentro de <task-card>. Recupera el componente de la lección "Tu Primer Componente Lit" y modifícalo así:
import { LitElement, html } from 'lit';
class TaskCard extends LitElement {
constructor() {
super();
// Un campo de instancia simple, todavía sin el sistema
// de propiedades reactivas de Lit (eso llega en el módulo 3).
this.titulo = 'Preparar la demo del sprint';
}
render() {
return html`
<article>
<h3>${this.titulo}</h3>
<p>Estado: En curso</p>
<p>Asignada a: Ana</p>
</article>
`;
}
}
customElements.define('task-card', TaskCard);El cambio respecto a la versión anterior es pequeño pero importante: en el constructor se define this.titulo como un campo de instancia normal de JavaScript, y en render() se interpola ${this.titulo} dentro del <h3>, en lugar del texto fijo "Preparar la demo del sprint". Al recargar la página, el resultado visual es idéntico al de antes: se sigue mostrando "Preparar la demo del sprint", porque ese es el valor que se ha asignado a this.titulo. La diferencia no está en lo que se ve, sino en cómo se genera: ahora el título proviene de una expresión JavaScript (${this.titulo}), no de texto quemado directamente en la plantilla.
Puedes comprobarlo modificando el valor a mano, por ejemplo en la consola del navegador, obteniendo primero una referencia al elemento:
// Desde la consola del navegador (con <task-card> ya en la página):
const tarjeta = document.querySelector('task-card');
tarjeta.titulo = 'Revisar el backlog';Si ejecutas esto y no ocurre nada visible en la pantalla, no te preocupes: es exactamente el comportamiento esperado en este punto del curso, y se explica en el siguiente apartado.
- Importante: esto todavía no es reactividad completa
Es fundamental dejar claro un matiz antes de seguir avanzando en el módulo, para no generar expectativas equivocadas: this.titulo en el ejemplo anterior es, de momento, un simple campo de instancia de JavaScript, sin ninguna relación todavía con el sistema de propiedades reactivas que ofrece Lit. Si ejecutaste el código de la consola del apartado anterior (tarjeta.titulo = 'Revisar el backlog'), habrás comprobado que la tarjeta en pantalla no cambia, aunque el valor de this.titulo sí se haya actualizado internamente.
Esto ocurre porque, tal como se ha explicado en el apartado 3, Lit solo vuelve a ejecutar render() y actualizar el DOM cuando detecta que una propiedad reactiva ha cambiado, algo que todavía no se ha declarado en este componente. Declarar titulo como una propiedad reactiva de verdad (con static properties, como se apuntó en la lección de anatomía del módulo 1) es precisamente el contenido central del módulo 3, "Propiedades y Estado Reactivo".
Durante el resto de este módulo 2, seguirás usando campos de instancia simples como this.titulo para poder centrarte, sin distracciones, en cómo se escriben las plantillas: cómo interpolar varios tipos de valores, cómo renderizar condicionalmente, cómo renderizar listas y cómo funciona el ciclo de renderizado. El propio render() se ejecutará explícitamente cuando lo necesites para ver los cambios (por ejemplo, llamando de forma manual a un método que verás en la última lección del módulo), pero la actualización automática al cambiar un valor —la verdadera reactividad— es terreno del módulo 3. Tenlo presente: si algo no se actualiza "solo" en los próximos ejemplos, es intencionado y no un error.
Errores Comunes y Consejos
- Pensar que
htmldevuelve una cadena de texto: es un error muy extendido al empezar con Lit.html\...`devuelve un objetoTemplateResult, no una cadena. Si intentas concatenarlo con+` o usarlo donde se espera texto, no obtendrás el resultado esperado. - Esperar que cambiar un campo de instancia normal actualice la pantalla: como se ha explicado en el apartado 7, esto es exactamente lo que ocurre en este punto del curso, a propósito. La actualización automática llega con las propiedades reactivas del módulo 3.
- Confundir la caché de plantillas con caché de datos: lo que Lit cachea es el análisis de la estructura de la plantilla (dónde están los huecos), no los valores concretos que se han interpolado en un momento dado. Cada renderizado sigue usando los valores actuales.
- Usar
innerHTML"a mano" dentro de un componente Lit para mezclar enfoques: si necesitas mostrar HTML dinámico, hazlo siempre a través de la funciónhtmly sus interpolaciones, no mezclando manipulación directa del DOM con el sistema de plantillas de Lit; mezclar ambos enfoques puede confundir a Lit sobre qué nodos gestiona él y cuáles no.
Ejercicios
- Explica con tus propias palabras (dos o tres frases) qué son los dos arrays que recibe una función etiquetada como
html, y por qué esa separación es importante para el rendimiento de Lit. - Añade a
<task-card>un segundo campo de instancia,this.estado, inicializado a'En curso'en el constructor, e interpólalo en la plantilla sustituyendo el texto fijo "En curso" del párrafo de estado. - Desde la consola del navegador, cambia
tarjeta.estadoa otro valor y comprueba que, efectivamente, la pantalla no se actualiza. Anota en un comentario por qué ocurre esto, relacionándolo con lo explicado en el apartado 7.
Soluciones
-
Una función etiquetada como
htmlrecibe, por un lado, un array con los fragmentos de texto literal de la plantilla (las partes que nunca cambian) y, por otro, un array con los valores que se han interpolado con${}. Esta separación es importante porque permite a Lit analizar una sola vez el "esqueleto" fijo de la plantilla (dónde están los huecos) y, en renderizados posteriores, reutilizar ese análisis sin volver a parsear el HTML, insertando directamente los nuevos valores en los huecos ya conocidos.
import { LitElement, html } from 'lit';
class TaskCard extends LitElement {
constructor() {
super();
this.titulo = 'Preparar la demo del sprint';
this.estado = 'En curso';
}
render() {
return html`
<article>
<h3>${this.titulo}</h3>
<p>Estado: ${this.estado}</p>
<p>Asignada a: Ana</p>
</article>
`;
}
}
customElements.define('task-card', TaskCard);- Al ejecutar
tarjeta.estado = 'Bloqueada'desde la consola, la propiedad interna del objeto cambia (se puede comprobar conconsole.log(tarjeta.estado), que mostrará'Bloqueada'), pero la pantalla sigue mostrando "Estado: En curso". Esto ocurre porqueestado, igual quetitulo, es en este momento un campo de instancia normal de JavaScript, no una propiedad reactiva registrada ante Lit; Lit solo vuelve a ejecutarrender()cuando detecta cambios en propiedades que ha sido informado de vigilar, algo que se configurará constatic propertiesen el módulo 3.
Conclusión
En esta lección has abierto el módulo 2 desmontando el mecanismo interno de la función html: es una función etiquetada estándar de JavaScript que separa los trozos fijos de una plantilla de sus valores dinámicos, lo que permite a Lit analizar la estructura una sola vez y, en cada renderizado posterior, actualizar únicamente los huecos concretos del DOM real, sin necesidad de un Virtual DOM ni de reconstruir el HTML con innerHTML. Has dado además el primer paso práctico interpolando campos de instancia simples en <task-card>, dejando claro que esto todavía no activa ninguna actualización automática: esa pieza llega con las propiedades reactivas del módulo 3.
En la siguiente lección, "Expresiones e Interpolación en Plantillas", profundizarás en los distintos tipos de valores que se pueden interpolar dentro de una plantilla html —texto, atributos, propiedades del DOM con .prop, valores booleanos con ?attr— y ampliarás <task-card> para mostrar varios campos a la vez, incluyendo una primera pincelada de expresiones JavaScript más elaboradas dentro de ${}.
Curso de Lit
Módulo 1: Introducción a Lit y Web Components
- ¿Qué son los Web Components y por qué Lit?
- Configuración del Entorno de Desarrollo
- Tu Primer Componente Lit
- Anatomía de un Componente Lit
Módulo 2: Plantillas Reactivas y Renderizado
- El Motor de Plantillas de Lit
- Expresiones e Interpolación en Plantillas
- Renderizado Condicional
- Renderizado de Listas
- El Ciclo de Renderizado
Módulo 3: Propiedades y Estado Reactivo
- Propiedades Reactivas
- Estado Interno con @state
- Tipos de Propiedades y Conversores Personalizados
- Atributos vs Propiedades y Reflexión
Módulo 4: Estilos en Componentes Lit
- CSS Encapsulado con Shadow DOM
- Estilos Compartidos entre Componentes
- Variables CSS Personalizadas y Theming
- Slots y Estilizado de Contenido Distribuido
Módulo 5: Eventos y Comunicación entre Componentes
- Manejo de Eventos DOM en Plantillas
- Eventos Personalizados: Comunicación Hijo a Padre
- Comunicación de Padre a Hijo con Propiedades
- Patrones de Comunicación entre Componentes Hermanos
Módulo 6: Ciclo de Vida y Comportamiento Avanzado
- Callbacks del Ciclo de Vida
- Hooks Reactivos: willUpdate, updated y firstUpdated
- Controladores Reactivos
- Mixins y Composición de Comportamiento
Módulo 7: Directivas y Funcionalidades Avanzadas de Plantillas
- Directivas Incorporadas: classMap, styleMap e ifDefined
- Directivas Personalizadas
- Renderizado Asíncrono con until
- Contexto Compartido con @lit/context
Módulo 8: Integración, Interoperabilidad y Despliegue
- Usar Componentes Lit en HTML Plano
- Integrar Lit con React, Vue y Angular
- Renderizado en el Servidor con @lit-labs/ssr
- Empaquetado, Publicación y TypeScript
Módulo 9: Pruebas y Buenas Prácticas
- Pruebas Unitarias con Web Test Runner
- Accesibilidad en Web Components
- Rendimiento y Optimización
- Patrones y Anti-patrones Comunes
