A lo largo del curso han ido apareciendo, de pasada, varias decisiones que tenían algo que ver con el rendimiento: repeat con una clave estable en lugar de Array.map para listas que cambian, willUpdate para no recalcular lo mismo en cada render(), un bundle deliberadamente pequeño gracias al diseño minimalista de Lit. Cada una se explicó en su momento centrada en el problema concreto que resolvía, sin detenerse en el hilo común que las conecta. Esta lección retoma esas piezas ya conocidas, las pone una junto a la otra con un criterio de rendimiento explícito, y las aplica revisando cómo se comportarían <task-list> y <task-board> frente a una lista de tareas mucho más grande que los tres o cuatro ejemplos manejados hasta ahora.
Contenido
- Por qué el rendimiento de un componente reactivo no es un tema aparte
- Minimizar trabajo dentro de
render() - El coste oculto de crear funciones y objetos nuevos en cada render
repeatconkey: recapitulación con una lista grande de verdadwillUpdatepara cálculo derivado: recapitulación con criterio de coste- El tamaño del bundle, recordado desde la óptica del rendimiento
- Simulando una lista grande en TaskFlow
- Afinando
render()de<task-list>y<task-board> - Cuándo ninguna de estas técnicas basta: virtualización
- Por qué el rendimiento de un componente reactivo no es un tema aparte
En un componente Lit, el rendimiento no es una capa que se añade al final, separada del resto del diseño: está entrelazado con las mismas decisiones que ya se han tomado durante el curso sobre qué vive en una propiedad reactiva, qué vive en render() y qué vive en un hook del ciclo de actualización. Un componente mal diseñado en términos de responsabilidades —lógica de negocio mezclada con la plantilla, cálculos repetidos sin necesidad— casi siempre resulta también, como efecto secundario, un componente lento; y, a la inversa, las técnicas que mejoran el rendimiento (mover un cálculo a willUpdate, dar una identidad estable a cada elemento de una lista) suelen coincidir con las que ya se recomendaban por claridad y corrección, no solo por velocidad.
Esta lección no introduce ningún concepto nuevo de Lit: reordena y aplica con intención cuatro piezas que el curso ya ha explicado por separado, ahora bajo la pregunta explícita de "¿qué le cuesta al navegador, y cómo se reduce ese coste sin sacrificar nada del comportamiento ya construido?".
- Minimizar trabajo dentro de
render()
render()render() se ejecuta, potencialmente, en cada actualización de un componente: cada cambio de una única propiedad reactiva, aunque sea diminuto, dispara una nueva llamada completa a este método. Cualquier cálculo costoso colocado directamente dentro de render() se repite, por tanto, con la misma frecuencia que el propio renderizado, incluso en actualizaciones que no tienen ninguna relación con ese cálculo en particular.
// Evitar: recalcula algo costoso en cada render(), sin importar qué haya cambiado
render() {
const tareasOrdenadas = [...this.tareas].sort((a, b) => b.prioridad - a.prioridad);
return html`
<ul>
${tareasOrdenadas.map((tarea) => html`<li>${tarea.titulo}</li>`)}
</ul>
`;
}Para una lista de tres o cuatro elementos, como las que ha manejado TaskFlow durante la mayor parte del curso, ordenar de nuevo en cada render() es, en la práctica, gratis: ningún usuario notaría la diferencia. Pero el mismo patrón, aplicado a una lista de varios miles de tareas (el escenario que se explora en el apartado 7), convierte una operación de orden O(n log n) en un coste que se repite en cada tecla pulsada en el filtro, cada cambio de prioridad de una sola tarea, cada actualización que, por cualquier motivo, dispare un nuevo render() del componente que contiene esta lógica. El criterio general, ya apuntado por la lección "Renderizado de Listas" del módulo 2, es que render() debería limitarse, en la medida de lo posible, a transformar datos ya preparados en HTML, no a preparar esos datos desde cero cada vez.
- El coste oculto de crear funciones y objetos nuevos en cada render
Un segundo patrón, más sutil, aparece cada vez que una plantilla crea una función nueva —normalmente una arrow function— directamente dentro de una expresión de render():
${['todas', 'pendiente', 'hecha'].map(
(opcion) => html`
<button @click="${() => this.manejarEstado(opcion)}">
${opcion}
</button>
`
)}Este es, de hecho, exactamente el código de <task-filter> presentado en la lección "Contexto Compartido con @lit/context": cada vez que render() se ejecuta, map construye tres arrow functions nuevas, una por cada botón, ninguna de las cuales es la misma referencia de función que existía en el renderizado anterior. Lit, al aplicar el resultado al DOM, detecta que el manejador de evento ha "cambiado" (aunque haga exactamente lo mismo) y sustituye el listener anterior por el nuevo, en lugar de reutilizarlo. Para tres botones, este coste es insignificante; para una lista de cientos o miles de elementos, cada uno con su propio manejador en línea, el coste de crear y sustituir esas funciones en cada actualización empieza a notarse.
La alternativa, cuando es posible, es usar un único método enlazado de la clase en lugar de una función creada en cada iteración:
manejarEstado(event) {
const estado = event.currentTarget.dataset.estado;
this.valorActual.actualizar({ estado });
}
render() {
const { estado } = this.valorActual;
return html`
${['todas', 'pendiente', 'hecha'].map(
(opcion) => html`
<button data-estado="${opcion}" @click="${this.manejarEstado}" class="${classMap({ activo: estado === opcion })}">
${opcion}
</button>
`
)}
`;
}Aquí, @click="${this.manejarEstado}" referencia siempre el mismo método de la clase (Lit conserva automáticamente el valor de this dentro de los manejadores declarados así, como ya se explicó en la lección "Manejo de Eventos DOM en Plantillas"), sin crear ninguna función nueva en cada render(); el dato que antes se capturaba mediante el cierre de la arrow function (opcion) se recupera ahora desde event.currentTarget.dataset.estado, usando un atributo data-* sobre el propio botón. Esta reescritura tiene una contrapartida real que conviene sopesar, no aplicar a ciegas: el código resulta algo menos directo de leer que la versión con arrow function en línea, y para volúmenes de elementos pequeños (los tres botones de <task-filter>, o el manejador @tarea-cambiada con tarea.id capturado en la lección "Comunicación de Padre a Hijo con Propiedades") la ganancia de rendimiento es, en la práctica, indetectable. El criterio razonable es reservar este tipo de reescritura para listas que de verdad crecen (decenas o cientos de elementos), no aplicarlo de forma sistemática a los tres botones de <task-filter>, donde la claridad de la arrow function en línea sigue mereciendo la pena frente a un ahorro que nadie notaría.
repeat con key: recapitulación con una lista grande de verdad
repeat con key: recapitulación con una lista grande de verdadLa lección "Renderizado de Listas" del módulo 2 introdujo el problema de identidad al reordenar una lista renderizada con Array.map, y la lección "Contexto Compartido con @lit/context" del módulo 7 aplicó por fin repeat con tarea.id como clave dentro de <task-list>, precisamente para que el filtro pudiera insertar y eliminar tarjetas visibles sin perder el estado interno de las que permanecían. Con una lista de tres o cuatro tareas, la diferencia entre map y repeat es, en términos de coste puro, insignificante; con una lista de varios miles de tareas, la diferencia se vuelve determinante.
| Escenario | Con Array.map |
Con repeat + clave |
|---|---|---|
| El filtro deja fuera 500 de 2000 tareas | Lit compara por posición: puede llegar a reconstruir gran parte de las 1500 tarjetas visibles, aunque la mayoría sean las mismas tareas de antes en otra posición | Lit reconoce, por id, qué tarjetas son las mismas de antes; solo destruye los nodos de las 500 que salen del resultado filtrado |
| Se inserta una tarea nueva al principio de 2000 | Las 2000 posiciones se desplazan; riesgo de reconstrucción extensa | Solo se crea un nodo nuevo al principio; los 2000 existentes no se tocan |
Estado interno de una tarjeta (expandida) mientras el filtro cambia |
Puede perderse si Lit reutiliza el nodo físico de esa posición para otra tarea distinta | Se conserva mientras la tarea siga cumpliendo el filtro, independientemente de su posición |
<task-list> ya usa repeat desde el módulo 7, así que no hace falta ningún cambio de código en este apartado; lo que aporta esta lección es la magnitud real de esa decisión, visible solo cuando el volumen de datos deja de ser el puñado de tareas de ejemplo manejado durante la mayor parte del curso.
willUpdate para cálculo derivado: recapitulación con criterio de coste
willUpdate para cálculo derivado: recapitulación con criterio de costeLa lección "Hooks Reactivos" del módulo 6 presentó willUpdate como el lugar correcto para recalcular cercaDeVencer únicamente cuando fechaLimite cambia, en lugar de en cada render(), y cerró aquel apartado señalando el coste de la alternativa: recalcular en cada renderizado, incluidos los que no tienen nada que ver con la fecha límite. Esa misma lógica, aplicada ahora a la pregunta de esta lección, es un ejemplo perfecto del criterio general del apartado 2: cualquier cálculo derivado que dependa de un subconjunto concreto de propiedades debería vivir en willUpdate, protegido por changedProperties.has(...), no repetirse sin condición dentro de render().
El mismo razonamiento se aplica a tareasFiltradas en <task-list> (lección "Contexto Compartido con @lit/context"), declarado como un getter que recalcula el filtrado completo cada vez que se lee, incluso desde dentro del propio render():
// Tal como quedó en el módulo 7: se recalcula cada vez que se lee
get tareasFiltradas() {
const { texto, estado } = this._filtro.value ?? { texto: '', estado: 'todas' };
const textoNormalizado = texto.toLowerCase();
return this.tareas.filter((tarea) => {
const coincideEstado = estado === 'todas' || tarea.estado === estado;
const coincideTexto = tarea.titulo.toLowerCase().includes(textoNormalizado);
return coincideEstado && coincideTexto;
});
}Para el volumen de tareas manejado hasta ahora, este getter es perfectamente razonable tal como está: se lee una única vez por render() (no varias veces dentro del mismo método, lo que sí duplicaría el coste sin necesidad), y el propio filtrado, sobre pocos elementos, es prácticamente instantáneo. El apartado 7 retoma este mismo getter frente a una lista mucho más grande, para decidir con datos si conviene moverlo a willUpdate o si, incluso a mayor escala, sigue siendo aceptable dejarlo como está.
- El tamaño del bundle, recordado desde la óptica del rendimiento
La lección "Empaquetado, Publicación y TypeScript" del módulo 8 explicó por qué Lit es deliberadamente pequeño y por qué eso beneficia el tiempo de carga inicial de TaskFlow. Aquella explicación se centraba en el tiempo hasta que el primer componente queda definido; el mismo razonamiento tiene una segunda cara relevante para esta lección: cuantas más dependencias añade un proyecto por encima de Lit (una librería de utilidades genérica para tratar arrays, un framework de componentes de UI adicional, una librería de iconos completa cuando solo se usan tres iconos), mayor es el bundle final, y mayor el tiempo que el navegador necesita para descargarlo, analizarlo y ejecutarlo antes de que cualquier componente de TaskFlow —incluidas las optimizaciones de este mismo módulo— pueda siquiera empezar a renderizarse. Ninguna optimización de render() o de repeat compensa un bundle inicial innecesariamente grande: son preocupaciones complementarias, no sustitutivas, y ambas conviene revisar antes de considerar cerrado el rendimiento de una aplicación.
- Simulando una lista grande en TaskFlow
Para razonar con datos reales, en lugar de con intuiciones, resulta útil generar una lista de tareas de un tamaño muy superior al manejado hasta ahora y observar el comportamiento de <task-list> frente a ella:
function generarTareasDeEjemplo(cantidad) {
const estados = ['pendiente', 'en-progreso', 'hecha'];
return Array.from({ length: cantidad }, (_, indice) => ({
id: indice + 1,
titulo: `Tarea de ejemplo número ${indice + 1}`,
estado: estados[indice % estados.length],
prioridad: (indice % 5) + 1,
urgente: indice % 7 === 0,
}));
}
const board = document.querySelector('task-board');
board.tareas = generarTareasDeEjemplo(2000);Con 2000 tareas asignadas de golpe a <task-board> (que las reenvía, como ya hace desde el módulo 5, hacia <task-list>), el primer renderizado completo —2000 instancias de <task-card>, cada una con su propio Shadow DOM, su propio <user-avatar> interno y su propio ContadorTiempoRestanteController— es, con diferencia, el momento más costoso de toda la interacción: crear miles de elementos personalizados de golpe, cada uno con su propio ciclo de vida completo, tiene un coste real que ninguna de las técnicas de esta lección elimina por completo, porque no depende de cómo se recorre la lista sino de cuántos componentes distintos hay que instanciar. Las secciones siguientes se centran, en cambio, en lo que sí se puede controlar: qué ocurre en las actualizaciones posteriores a ese primer renderizado, que es donde repeat, willUpdate y evitar trabajo innecesario en render() marcan la diferencia real.
- Afinando
render() de <task-list> y <task-board>
render() de <task-list> y <task-board>Con las 2000 tareas ya cargadas, escribir un carácter en el campo de búsqueda de <task-filter> dispara, en cascada, una nueva evaluación de tareasFiltradas en <task-list> cada vez que ese componente vuelve a renderizarse. El getter del apartado 5, tal como está, recorre el array completo de 2000 tareas en cada pulsación de tecla; con Array.filter, ese recorrido es de coste lineal (O(n)) respecto al número total de tareas, no respecto al número de tareas visibles, así que el coste no depende de cuántos resultados queden, sino de cuántas tareas existan en total.
Para esta escala, ese coste lineal sigue siendo aceptable en la práctica (un filtro sobre 2000 elementos con comparaciones simples de texto y de igualdad se ejecuta, en cualquier navegador moderno, en un tiempo del orden de un milisegundo, muy por debajo del umbral perceptible por una persona), así que no hace falta mover tareasFiltradas a willUpdate en este caso concreto: sería una optimización real, pero resolviendo un problema que, a esta escala, no está causando ninguna lentitud perceptible. Aplicar el criterio del apartado 1 con honestidad significa, aquí, reconocer que la técnica ya conocida (willUpdate) sigue disponible, pero que introducirla sin que exista un problema medible añadiría complejidad sin ningún beneficio real.
Donde sí aparece una diferencia perceptible, en cambio, es exactamente en el apartado 4: comprobar, con las herramientas de desarrollador del navegador, cuántos nodos DOM se crean o se destruyen al escribir en el filtro sobre las 2000 tareas. Con repeat y tarea.id como clave —ya en uso desde el módulo 7—, escribir una letra que reduce el resultado de 2000 a 340 coincidencias destruye únicamente los nodos de las 1660 tarjetas que dejan de cumplir el filtro, sin tocar los nodos de las 340 que permanecen visibles; revirtiendo el cambio (borrando la letra escrita), esas mismas 1660 tarjetas se recrean, no se recuperan de ningún tipo de caché, porque repeat no mantiene en memoria los nodos de elementos que ya no aparecen en el array recibido. Esta observación no exige ningún cambio de código adicional sobre lo ya construido en el módulo 7: confirma, con una escala de datos mucho mayor, que la decisión tomada entonces era la correcta, y que sustituir repeat por Array.map a esta escala sí sería perceptible, al forzar comparaciones por posición sobre miles de elementos en cada tecla.
- Cuándo ninguna de estas técnicas basta: virtualización
Las técnicas de esta lección reducen el coste de actualizar una lista larga que cambia, pero no reducen el coste del primer renderizado completo señalado en el apartado 7: crear 2000 instancias de <task-card> de golpe sigue siendo costoso, sin importar cuán bien optimizado esté el resto del código. Para volúmenes de datos donde ese primer coste se vuelve inaceptable (decenas de miles de elementos, no los miles de este ejemplo), la técnica habitual, mencionada aquí solo de forma orientativa y fuera del alcance práctico de este curso, es la virtualización: renderizar en el DOM únicamente los elementos que caen dentro (o cerca) del área visible en cada momento, y crear o destruir el resto dinámicamente a medida que el usuario se desplaza por la lista, en lugar de mantener miles de nodos reales existentes simultáneamente aunque la inmensa mayoría no sea visible en ningún momento dado. TaskFlow, con los volúmenes de datos razonables para una aplicación de gestión de tareas de un equipo, no necesita llegar a ese extremo, pero conviene saber que la técnica existe si el proyecto creciera mucho más allá de lo que este curso cubre.
Errores Comunes y Consejos
- Optimizar sin medir primero: como se ha visto en el apartado 8, mover
tareasFiltradasawillUpdatesin comprobar antes si el coste real delfiltersobre el volumen de datos actual es perceptible añade complejidad sin ningún beneficio demostrado; medir antes de optimizar evita este tipo de esfuerzo desperdiciado. - Sustituir toda arrow function en línea por un método enlazado "por si acaso": como se ha explicado en el apartado 3, esta reescritura tiene un coste real de legibilidad, y solo aporta un beneficio medible cuando el número de elementos afectados es alto; aplicarla a los tres botones de
<task-filter>sería exactamente el mismo error de optimizar sin necesidad real. - Confundir el coste del primer renderizado con el coste de actualizaciones posteriores: como se ha señalado en el apartado 7, ninguna técnica de esta lección reduce el coste de crear miles de componentes por primera vez;
repeat,willUpdatey evitar trabajo innecesario enrender()optimizan las actualizaciones que vienen después de ese primer renderizado, no lo sustituyen. - Recalcular el mismo getter varias veces dentro del mismo
render(): sirender()llamara athis.tareasFiltradasdos o tres veces (por ejemplo, una para contar el resultado y otra para iterarlo), el coste del filtrado se multiplicaría sin ninguna necesidad; conviene leer el getter una sola vez en una variable local y reutilizar esa variable dentro del mismorender().
Ejercicios
- Reescribe el getter
tareasFiltradasde<task-list>para que, en lugar de ejecutarse en cada lectura, se recalcule dentro dewillUpdateúnicamente cuandochangedPropertiesincluyatareas, guardando el resultado en un estado interno_tareasFiltradasCache. Explica, apoyándote en el apartado 8, en qué escenario de uso real esta versión dejaría de ser una mejora y empezaría a introducir un problema (pista: piensa en qué dispara hoy un nuevo filtrado quetareaspor sí solo no capturaría). - Un compañero de equipo, tras leer el apartado 3, reescribe todos los manejadores de clic en línea de TaskFlow (incluido el de
<task-filter>y el de "Eliminar tarea" de<task-card>) como métodos enlazados con atributosdata-*, sin medir antes ningún impacto real. Explica, basándote en el apartado 1 y en el propio apartado 3, si esta decisión está justificada tal como se describe. - Explica, basándote en el apartado 9, por qué virtualizar
<task-list>no resolvería, por sí sola, el coste del primer renderizado descrito en el apartado 7 si las 2000 tareas se asignaran todas de golpe y la lista completa (sin ningún desplazamiento del usuario) se mostrara igualmente entera desde el primer instante.
Soluciones
static properties = {
tareas: { type: Array },
_tareasFiltradasCache: { state: true },
};
willUpdate(changedProperties) {
if (changedProperties.has('tareas')) {
this._tareasFiltradasCache = this._calcularTareasFiltradas();
}
}
_calcularTareasFiltradas() {
const { texto, estado } = this._filtro.value ?? { texto: '', estado: 'todas' };
const textoNormalizado = texto.toLowerCase();
return this.tareas.filter((tarea) => {
const coincideEstado = estado === 'todas' || tarea.estado === estado;
const coincideTexto = tarea.titulo.toLowerCase().includes(textoNormalizado);
return coincideEstado && coincideTexto;
});
}El problema de esta versión es que el filtro no cambia únicamente cuando tareas cambia: también cambia cuando el usuario escribe en <task-filter> o pulsa uno de sus botones, un cambio que llega a <task-list> a través del ContextConsumer suscrito al contexto de filtro, no como una propiedad reactiva declarada en static properties de <task-list>. willUpdate con changedProperties.has('tareas') nunca vería ese segundo tipo de cambio (el contexto no dispara, por sí solo, un changedProperties con la clave tareas), así que la caché quedaría desactualizada en cuanto el usuario tocara el filtro, mostrando siempre el resultado del filtro anterior hasta que tareas cambiara por otro motivo. Resolverlo correctamente exigiría, como mínimo, recalcular también cuando el valor del contexto cambie, lo que en la práctica devuelve buena parte de la complejidad que esta "optimización" pretendía evitar, reforzando la conclusión del apartado 8: para el volumen actual de TaskFlow, el getter original, sin caché, sigue siendo la opción más simple y suficientemente rápida.
- No está justificada tal como se describe. El criterio del apartado 1 exige medir antes de optimizar, y el del apartado 3 es explícito en que esta reescritura solo aporta un beneficio real para listas con un número alto de elementos; ni los tres botones de
<task-filter>ni el único botón "Eliminar tarea" de cada<task-card>(que ya se crea una sola vez por tarjeta, no en un bucle interno) encajan en ese perfil. Aplicar la reescritura de forma sistemática, sin medir, sacrifica la claridad de las arrow functions en línea a cambio de un ahorro de rendimiento indetectable en la práctica, exactamente el primer error señalado en la lista de errores comunes de esta lección. - La virtualización reduce el número de elementos que existen simultáneamente en el DOM en un momento dado, creando y destruyendo nodos a medida que el usuario se desplaza; pero si las 2000 tareas deben mostrarse todas visibles desde el primer instante, sin ningún desplazamiento que limite qué parte de la lista es relevante en cada momento, la virtualización no tiene ningún elemento que "no mostrar todavía": el propio requisito de la interfaz (ver las 2000 de golpe) obliga a crear los 2000 componentes reales sin importar la técnica de renderizado usada. La virtualización ayuda precisamente cuando la mayoría del contenido no es visible en un momento dado (listas muy largas donde el usuario solo ve una pequeña ventana a la vez); no ayuda cuando el propio diseño de la interfaz exige mostrarlo todo simultáneamente.
Conclusión
Esta lección no ha introducido ningún concepto nuevo de Lit, sino que ha puesto en perspectiva, con un criterio de rendimiento explícito, cuatro decisiones que TaskFlow ya había tomado por otros motivos a lo largo del curso: minimizar trabajo dentro de render(), medir antes de sustituir funciones en línea por métodos enlazados, confiar en repeat con clave para listas que cambian de tamaño, y recordar que ninguna de estas técnicas sustituye a un bundle inicial razonablemente pequeño. Frente a una lista simulada de 2000 tareas, esas decisiones —tomadas ya en los módulos 2, 6, 7 y 8— han demostrado seguir siendo las correctas, sin necesidad de reescribir nada adicional salvo donde el propio análisis lo ha justificado con datos, no con intuición.
Con el rendimiento ya revisado con criterio, queda una última lección en este módulo antes del proyecto final: un repaso transversal de patrones recomendados frente a anti-patrones, que recorre TaskFlow de principio a fin y sirve de puente directo hacia el módulo de cierre del curso.
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
