Al llarg del curs han anat apareixent, de passada, diverses decisions que tenien alguna cosa a veure amb el rendiment: repeat amb una clau estable en lloc d'Array.map per a llistes que canvien, willUpdate per no recalcular el mateix a cada render(), un bundle deliberadament petit gràcies al disseny minimalista de Lit. Cadascuna es va explicar en el seu moment centrada en el problema concret que resolia, sense aturar-se en el fil comú que les connecta. Aquesta lliçó recupera aquestes peces ja conegudes, les posa una al costat de l'altra amb un criteri de rendiment explícit, i les aplica revisant com es comportarien <task-list> i <task-board> enfront d'una llista de tasques molt més gran que els tres o quatre exemples manejats fins ara.
Contingut
- Per què el rendiment d'un component reactiu no és un tema a part
- Minimitzar treball dins de
render() - El cost ocult de crear funcions i objectes nous a cada render
repeatambkey: recapitulació amb una llista gran de veritatwillUpdateper a càlcul derivat: recapitulació amb criteri de cost- La mida del bundle, recordada des de l'òptica del rendiment
- Simulant una llista gran a TaskFlow
- Afinant
render()de<task-list>i<task-board> - Quan cap d'aquestes tècniques és suficient: virtualització
- Per què el rendiment d'un component reactiu no és un tema a part
En un component Lit, el rendiment no és una capa que s'afegeix al final, separada de la resta del disseny: està entrellaçat amb les mateixes decisions que ja s'han pres durant el curs sobre què viu en una propietat reactiva, què viu en render() i què viu en un hook del cicle d'actualització. Un component mal dissenyat en termes de responsabilitats —lògica de negoci mesclada amb la plantilla, càlculs repetits sense necessitat— gairebé sempre resulta també, com a efecte secundari, un component lent; i, a l'inrevés, les tècniques que milloren el rendiment (moure un càlcul a willUpdate, donar una identitat estable a cada element d'una llista) solen coincidir amb les que ja es recomanaven per claredat i correcció, no només per velocitat.
Aquesta lliçó no introdueix cap concepte nou de Lit: reordena i aplica amb intenció quatre peces que el curs ja ha explicat per separat, ara sota la pregunta explícita de "què li costa al navegador, i com es redueix aquest cost sense sacrificar res del comportament ja construït?".
- Minimitzar treball dins de
render()
render()render() s'executa, potencialment, a cada actualització d'un component: cada canvi d'una única propietat reactiva, encara que sigui minúscul, dispara una nova crida completa a aquest mètode. Qualsevol càlcul costós col·locat directament dins de render() es repeteix, per tant, amb la mateixa freqüència que la pròpia renderització, fins i tot en actualitzacions que no tenen cap relació amb aquest càlcul en particular.
// Evitar: recalcula alguna cosa costosa a cada render(), sense importar què hagi canviat
render() {
const tareasOrdenadas = [...this.tareas].sort((a, b) => b.prioridad - a.prioridad);
return html`
<ul>
${tareasOrdenadas.map((tarea) => html`<li>${tarea.titulo}</li>`)}
</ul>
`;
}Per a una llista de tres o quatre elements, com les que ha manejat TaskFlow durant la major part del curs, ordenar de nou a cada render() és, a la pràctica, gratis: cap usuari notaria la diferència. Però el mateix patró, aplicat a una llista de diversos milers de tasques (l'escenari que s'explora a l'apartat 7), converteix una operació d'ordre O(n log n) en un cost que es repeteix a cada tecla premuda al filtre, a cada canvi de prioritat d'una sola tasca, a cada actualització que, per qualsevol motiu, dispari un nou render() del component que conté aquesta lògica. El criteri general, ja apuntat a la lliçó "Renderitzat de Llistes" del mòdul 2, és que render() hauria de limitar-se, en la mesura del possible, a transformar dades ja preparades en HTML, no a preparar aquestes dades des de zero cada vegada.
- El cost ocult de crear funcions i objectes nous a cada render
Un segon patró, més subtil, apareix cada vegada que una plantilla crea una funció nova —normalment una arrow function— directament dins d'una expressió de render():
${['todas', 'pendiente', 'hecha'].map(
(opcion) => html`
<button @click="${() => this.manejarEstado(opcion)}">
${opcion}
</button>
`
)}Aquest és, de fet, exactament el codi de <task-filter> presentat a la lliçó "Context Compartit amb @lit/context": cada vegada que render() s'executa, map construeix tres arrow functions noves, una per cada botó, cap de les quals és la mateixa referència de funció que existia en la renderització anterior. Lit, en aplicar el resultat al DOM, detecta que el gestor d'esdeveniment ha "canviat" (encara que faci exactament el mateix) i substitueix el listener anterior pel nou, en lloc de reutilitzar-lo. Per a tres botons, aquest cost és insignificant; per a una llista de centenars o milers d'elements, cadascun amb el seu propi gestor en línia, el cost de crear i substituir aquestes funcions a cada actualització comença a notar-se.
L'alternativa, quan és possible, és utilitzar un únic mètode enllaçat de la classe en lloc d'una funció creada a cada iteració:
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 sempre el mateix mètode de la classe (Lit conserva automàticament el valor de this dins dels gestors declarats així, com ja es va explicar a la lliçó "Gestió d'Esdeveniments DOM en Plantilles"), sense crear cap funció nova a cada render(); la dada que abans es capturava mitjançant el tancament de l'arrow function (opcion) es recupera ara des d'event.currentTarget.dataset.estado, utilitzant un atribut data-* sobre el propi botó. Aquesta reescriptura té una contrapartida real que convé sospesar, no aplicar a cegues: el codi resulta una mica menys directe de llegir que la versió amb arrow function en línia, i per a volums d'elements petits (els tres botons de <task-filter>, o el gestor @tarea-cambiada amb tarea.id capturat a la lliçó "Comunicació de Pare a Fill amb Propietats") el guany de rendiment és, a la pràctica, indetectable. El criteri raonable és reservar aquest tipus de reescriptura per a llistes que de veritat creixen (desenes o centenars d'elements), no aplicar-lo de forma sistemàtica als tres botons de <task-filter>, on la claredat de l'arrow function en línia continua mereixent la pena enfront d'un estalvi que ningú notaria.
repeat amb key: recapitulació amb una llista gran de veritat
repeat amb key: recapitulació amb una llista gran de veritatLa lliçó "Renderitzat de Llistes" del mòdul 2 va introduir el problema d'identitat en reordenar una llista renderitzada amb Array.map, i la lliçó "Context Compartit amb @lit/context" del mòdul 7 va aplicar finalment repeat amb tarea.id com a clau dins de <task-list>, precisament perquè el filtre pogués inserir i eliminar targetes visibles sense perdre l'estat intern de les que romanien. Amb una llista de tres o quatre tasques, la diferència entre map i repeat és, en termes de cost pur, insignificant; amb una llista de diversos milers de tasques, la diferència es torna determinant.
| Escenari | Amb Array.map |
Amb repeat + clau |
|---|---|---|
| El filtre deixa fora 500 de 2000 tasques | Lit compara per posició: pot arribar a reconstruir gran part de les 1500 targetes visibles, encara que la majoria siguin les mateixes tasques d'abans en una altra posició | Lit reconeix, per id, quines targetes són les mateixes d'abans; només destrueix els nodes de les 500 que surten del resultat filtrat |
| S'insereix una tasca nova al principi de 2000 | Les 2000 posicions es desplacen; risc de reconstrucció extensa | Només es crea un node nou al principi; els 2000 existents no es toquen |
Estat intern d'una targeta (expandida) mentre el filtre canvia |
Pot perdre's si Lit reutilitza el node físic d'aquella posició per a una tasca diferent | Es conserva mentre la tasca continuï complint el filtre, independentment de la seva posició |
<task-list> ja utilitza repeat des del mòdul 7, així que no cal cap canvi de codi en aquest apartat; el que aporta aquesta lliçó és la magnitud real d'aquesta decisió, visible només quan el volum de dades deixa de ser el grapat de tasques d'exemple manejat durant la major part del curs.
willUpdate per a càlcul derivat: recapitulació amb criteri de cost
willUpdate per a càlcul derivat: recapitulació amb criteri de costLa lliçó "Hooks Reactius" del mòdul 6 va presentar willUpdate com el lloc correcte per recalcular cercaDeVencer únicament quan fechaLimite canvia, en lloc de a cada render(), i va tancar aquell apartat assenyalant el cost de l'alternativa: recalcular a cada renderització, incloses les que no tenen res a veure amb la data límit. Aquesta mateixa lògica, aplicada ara a la pregunta d'aquesta lliçó, és un exemple perfecte del criteri general de l'apartat 2: qualsevol càlcul derivat que depengui d'un subconjunt concret de propietats hauria de viure a willUpdate, protegit per changedProperties.has(...), no repetir-se sense condició dins de render().
El mateix raonament s'aplica a tareasFiltradas a <task-list> (lliçó "Context Compartit amb @lit/context"), declarat com un getter que recalcula el filtratge complet cada vegada que es llegeix, fins i tot des de dins del propi render():
// Tal com va quedar al mòdul 7: es recalcula cada vegada que es llegeix
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;
});
}Per al volum de tasques manejat fins ara, aquest getter és perfectament raonable tal com està: es llegeix una única vegada per render() (no diverses vegades dins del mateix mètode, cosa que sí duplicaria el cost sense necessitat), i el propi filtratge, sobre pocs elements, és pràcticament instantani. L'apartat 7 recupera aquest mateix getter enfront d'una llista molt més gran, per decidir amb dades si convé moure'l a willUpdate o si, fins i tot a més escala, continua sent acceptable deixar-lo com està.
- La mida del bundle, recordada des de l'òptica del rendiment
La lliçó "Empaquetatge, Publicació i TypeScript" del mòdul 8 va explicar per què Lit és deliberadament petit i per què això beneficia el temps de càrrega inicial de TaskFlow. Aquella explicació se centrava en el temps fins que el primer component queda definit; el mateix raonament té una segona cara rellevant per a aquesta lliçó: com més dependències afegeix un projecte per sobre de Lit (una llibreria d'utilitats genèrica per tractar arrays, un framework de components d'interfície addicional, una llibreria d'icones completa quan només s'utilitzen tres icones), més gran és el bundle final, i més gran el temps que el navegador necessita per descarregar-lo, analitzar-lo i executar-lo abans que qualsevol component de TaskFlow —incloses les optimitzacions d'aquest mateix mòdul— pugui ni tan sols començar a renderitzar-se. Cap optimització de render() o de repeat compensa un bundle inicial innecessàriament gran: són preocupacions complementàries, no substitutives, i totes dues convé revisar-les abans de considerar tancat el rendiment d'una aplicació.
- Simulant una llista gran a TaskFlow
Per raonar amb dades reals, en lloc d'amb intuïcions, resulta útil generar una llista de tasques d'una mida molt superior a la manejada fins ara i observar el comportament de <task-list> enfront d'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);Amb 2000 tasques assignades de cop a <task-board> (que les reenvia, com ja fa des del mòdul 5, cap a <task-list>), la primera renderització completa —2000 instàncies de <task-card>, cadascuna amb el seu propi Shadow DOM, el seu propi <user-avatar> intern i el seu propi ContadorTiempoRestanteController— és, de llarg, el moment més costós de tota la interacció: crear milers d'elements personalitzats de cop, cadascun amb el seu propi cicle de vida complet, té un cost real que cap de les tècniques d'aquesta lliçó elimina del tot, perquè no depèn de com es recorre la llista sinó de quants components diferents cal instanciar. Les seccions següents se centren, en canvi, en allò que sí que es pot controlar: què passa a les actualitzacions posteriors a aquesta primera renderització, que és on repeat, willUpdate i evitar treball innecessari a render() marquen la diferència real.
- Afinant
render() de <task-list> i <task-board>
render() de <task-list> i <task-board>Amb les 2000 tasques ja carregades, escriure un caràcter al camp de cerca de <task-filter> dispara, en cascada, una nova avaluació de tareasFiltradas a <task-list> cada vegada que aquest component torna a renderitzar-se. El getter de l'apartat 5, tal com està, recorre l'array complet de 2000 tasques a cada pulsació de tecla; amb Array.filter, aquest recorregut és de cost lineal (O(n)) respecte al nombre total de tasques, no respecte al nombre de tasques visibles, així que el cost no depèn de quants resultats quedin, sinó de quantes tasques existeixin en total.
Per a aquesta escala, aquest cost lineal continua sent acceptable a la pràctica (un filtre sobre 2000 elements amb comparacions simples de text i d'igualtat s'executa, en qualsevol navegador modern, en un temps de l'ordre d'un mil·lisegon, molt per sota del llindar perceptible per una persona), així que no cal moure tareasFiltradas a willUpdate en aquest cas concret: seria una optimització real, però resolent un problema que, a aquesta escala, no està causant cap lentitud perceptible. Aplicar el criteri de l'apartat 1 amb honestedat significa, aquí, reconèixer que la tècnica ja coneguda (willUpdate) continua disponible, però que introduir-la sense que existeixi un problema mesurable afegiria complexitat sense cap benefici real.
On sí que apareix una diferència perceptible, en canvi, és exactament a l'apartat 4: comprovar, amb les eines de desenvolupador del navegador, quants nodes DOM es creen o es destrueixen en escriure al filtre sobre les 2000 tasques. Amb repeat i tarea.id com a clau —ja en ús des del mòdul 7—, escriure una lletra que redueix el resultat de 2000 a 340 coincidències destrueix únicament els nodes de les 1660 targetes que deixen de complir el filtre, sense tocar els nodes de les 340 que romanen visibles; revertint el canvi (esborrant la lletra escrita), aquestes mateixes 1660 targetes es recreen, no es recuperen de cap tipus de memòria cau, perquè repeat no manté en memòria els nodes d'elements que ja no apareixen a l'array rebut. Aquesta observació no exigeix cap canvi de codi addicional sobre allò ja construït al mòdul 7: confirma, amb una escala de dades molt més gran, que la decisió presa aleshores era la correcta, i que substituir repeat per Array.map a aquesta escala sí que seria perceptible, en forçar comparacions per posició sobre milers d'elements a cada tecla.
- Quan cap d'aquestes tècniques és suficient: virtualització
Les tècniques d'aquesta lliçó redueixen el cost d'actualitzar una llista llarga que canvia, però no redueixen el cost de la primera renderització completa assenyalada a l'apartat 7: crear 2000 instàncies de <task-card> de cop continua sent costós, sense importar com d'optimitzat estigui la resta del codi. Per a volums de dades on aquest primer cost es torna inacceptable (desenes de milers d'elements, no els milers d'aquest exemple), la tècnica habitual, mencionada aquí només de forma orientativa i fora de l'abast pràctic d'aquest curs, és la virtualització: renderitzar al DOM únicament els elements que cauen dins (o a prop) de l'àrea visible en cada moment, i crear o destruir la resta dinàmicament a mesura que l'usuari es desplaça per la llista, en lloc de mantenir milers de nodes reals existents simultàniament encara que la immensa majoria no siguin visibles en cap moment donat. TaskFlow, amb els volums de dades raonables per a una aplicació de gestió de tasques d'un equip, no necessita arribar a aquest extrem, però convé saber que la tècnica existeix si el projecte creixés molt més enllà d'allò que cobreix aquest curs.
Errors Comuns i Consells
- Optimitzar sense mesurar primer: com s'ha vist a l'apartat 8, moure
tareasFiltradasawillUpdatesense comprovar abans si el cost real delfiltersobre el volum de dades actual és perceptible afegeix complexitat sense cap benefici demostrat; mesurar abans d'optimitzar evita aquest tipus d'esforç malbaratat. - Substituir tota arrow function en línia per un mètode enllaçat "per si de cas": com s'ha explicat a l'apartat 3, aquesta reescriptura té un cost real de llegibilitat, i només aporta un benefici mesurable quan el nombre d'elements afectats és alt; aplicar-la als tres botons de
<task-filter>seria exactament el mateix error d'optimitzar sense necessitat real. - Confondre el cost de la primera renderització amb el cost d'actualitzacions posteriors: com s'ha assenyalat a l'apartat 7, cap tècnica d'aquesta lliçó redueix el cost de crear milers de components per primera vegada;
repeat,willUpdatei evitar treball innecessari arender()optimitzen les actualitzacions que vénen després d'aquella primera renderització, no la substitueixen. - Recalcular el mateix getter diverses vegades dins del mateix
render(): sirender()cridésthis.tareasFiltradasdues o tres vegades (per exemple, una per comptar el resultat i altra per iterar-lo), el cost del filtratge es multiplicaria sense cap necessitat; convé llegir el getter una sola vegada en una variable local i reutilitzar aquesta variable dins del mateixrender().
Exercicis
- Reescriu el getter
tareasFiltradasde<task-list>perquè, en lloc d'executar-se a cada lectura, es recalculi dins dewillUpdateúnicament quanchangedPropertiesincloguitareas, guardant el resultat en un estat intern_tareasFiltradasCache. Explica, recolzant-te en l'apartat 8, en quin escenari d'ús real aquesta versió deixaria de ser una millora i començaria a introduir un problema (pista: pensa en què dispara avui un nou filtratge quetareasper si sola no capturaria). - Un company d'equip, després de llegir l'apartat 3, reescriu tots els gestors de clic en línia de TaskFlow (inclòs el de
<task-filter>i el d'"Eliminar tarea" de<task-card>) com a mètodes enllaçats amb atributsdata-*, sense mesurar abans cap impacte real. Explica, basant-te en l'apartat 1 i en el propi apartat 3, si aquesta decisió està justificada tal com es descriu. - Explica, basant-te en l'apartat 9, per què virtualitzar
<task-list>no resoldria, per si sola, el cost de la primera renderització descrit a l'apartat 7 si les 2000 tasques s'assignessin totes de cop i la llista completa (sense cap desplaçament de l'usuari) es mostrés igualment sencera des del primer instant.
Solucions
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 d'aquesta versió és que el filtre no canvia únicament quan tareas canvia: també canvia quan l'usuari escriu a <task-filter> o prem un dels seus botons, un canvi que arriba a <task-list> a través del ContextConsumer subscrit al context de filtre, no com una propietat reactiva declarada a static properties de <task-list>. willUpdate amb changedProperties.has('tareas') mai veuria aquest segon tipus de canvi (el context no dispara, per si sol, un changedProperties amb la clau tareas), així que la memòria cau quedaria desactualitzada en el moment en què l'usuari tocés el filtre, mostrant sempre el resultat del filtre anterior fins que tareas canviés per un altre motiu. Resoldre-ho correctament exigiria, com a mínim, recalcular també quan el valor del context canviï, cosa que a la pràctica retorna bona part de la complexitat que aquesta "optimització" pretenia evitar, reforçant la conclusió de l'apartat 8: per al volum actual de TaskFlow, el getter original, sense memòria cau, continua sent l'opció més simple i suficientment ràpida.
- No està justificada tal com es descriu. El criteri de l'apartat 1 exigeix mesurar abans d'optimitzar, i el de l'apartat 3 és explícit en que aquesta reescriptura només aporta un benefici real per a llistes amb un nombre alt d'elements; ni els tres botons de
<task-filter>ni l'únic botó "Eliminar tarea" de cada<task-card>(que ja es crea una sola vegada per targeta, no en un bucle intern) encaixen en aquest perfil. Aplicar la reescriptura de forma sistemàtica, sense mesurar, sacrifica la claredat de les arrow functions en línia a canvi d'un estalvi de rendiment indetectable a la pràctica, exactament el primer error assenyalat a la llista d'errors comuns d'aquesta lliçó. - La virtualització redueix el nombre d'elements que existeixen simultàniament al DOM en un moment donat, creant i destruint nodes a mesura que l'usuari es desplaça; però si les 2000 tasques han de mostrar-se totes visibles des del primer instant, sense cap desplaçament que limiti quina part de la llista és rellevant en cada moment, la virtualització no té cap element que "no mostrar encara": el propi requisit de la interfície (veure les 2000 de cop) obliga a crear els 2000 components reals sense importar la tècnica de renderització utilitzada. La virtualització ajuda precisament quan la majoria del contingut no és visible en un moment donat (llistes molt llargues on l'usuari només veu una petita finestra alhora); no ajuda quan el propi disseny de la interfície exigeix mostrar-ho tot simultàniament.
Conclusió
Aquesta lliçó no ha introduït cap concepte nou de Lit, sinó que ha posat en perspectiva, amb un criteri de rendiment explícit, quatre decisions que TaskFlow ja havia pres per altres motius al llarg del curs: minimitzar treball dins de render(), mesurar abans de substituir funcions en línia per mètodes enllaçats, confiar en repeat amb clau per a llistes que canvien de mida, i recordar que cap d'aquestes tècniques substitueix un bundle inicial raonablement petit. Enfront d'una llista simulada de 2000 tasques, aquestes decisions —preses ja als mòduls 2, 6, 7 i 8— han demostrat continuar sent les correctes, sense necessitat de reescriure res addicional excepte on la pròpia anàlisi ho ha justificat amb dades, no amb intuïció.
Amb el rendiment ja revisat amb criteri, queda una última lliçó en aquest mòdul abans del projecte final: un repàs transversal de patrons recomanats enfront d'antipatrons, que recorre TaskFlow de principi a fi i serveix de pont directe cap al mòdul de tancament del curs.
Curs de Lit
Mòdul 1: Introducció a Lit i Web Components
- Què són els Web Components i per què Lit?
- Configuració de l'Entorn de Desenvolupament
- El Teu Primer Component Lit
- Anatomia d'un Component Lit
Mòdul 2: Plantilles Reactives i Renderitzat
- El Motor de Plantilles de Lit
- Expressions i Interpolació en Plantilles
- Renderitzat Condicional
- Renderitzat de Llistes
- El Cicle de Renderitzat
Mòdul 3: Propietats i Estat Reactiu
- Propietats Reactives
- Estat Intern amb @state
- Tipus de Propietats i Conversors Personalitzats
- Atributs vs Propietats i Reflexió
Mòdul 4: Estils en Components Lit
- CSS Encapsulat amb Shadow DOM
- Estils Compartits entre Components
- Variables CSS Personalitzades i Theming
- Slots i Estilitzat de Contingut Distribuït
Mòdul 5: Esdeveniments i Comunicació entre Components
- Gestió d'Esdeveniments DOM en Plantilles
- Esdeveniments Personalitzats: Comunicació de Fill a Pare
- Comunicació de Pare a Fill amb Propietats
- Patrons de Comunicació entre Components Germans
Mòdul 6: Cicle de Vida i Comportament Avançat
- Callbacks del Cicle de Vida
- Hooks Reactius: willUpdate, updated i firstUpdated
- Controladors Reactius
- Mixins i Composició de Comportament
Mòdul 7: Directives i Funcionalitats Avançades de Plantilles
- Directives Incorporades: classMap, styleMap i ifDefined
- Directives Personalitzades
- Renderitzat Asíncron amb until
- Context Compartit amb @lit/context
Mòdul 8: Integració, Interoperabilitat i Desplegament
- Utilitzar Components Lit en HTML Pla
- Integrar Lit amb React, Vue i Angular
- Renderitzat al Servidor amb @lit-labs/ssr
- Empaquetatge, Publicació i TypeScript
Mòdul 9: Proves i Bones Pràctiques
- Proves Unitàries amb Web Test Runner
- Accessibilitat en Web Components
- Rendiment i Optimització
- Patrons i Antipatrons Comuns
