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

  1. Per què el rendiment d'un component reactiu no és un tema a part
  2. Minimitzar treball dins de render()
  3. El cost ocult de crear funcions i objectes nous a cada render
  4. repeat amb key: recapitulació amb una llista gran de veritat
  5. willUpdate per a càlcul derivat: recapitulació amb criteri de cost
  6. La mida del bundle, recordada des de l'òptica del rendiment
  7. Simulant una llista gran a TaskFlow
  8. Afinant render() de <task-list> i <task-board>
  9. Quan cap d'aquestes tècniques és suficient: virtualització

  1. 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?".

  1. Minimitzar treball dins de 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.

  1. 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.

  1. repeat amb key: recapitulació amb una llista gran de veritat

La 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.

  1. willUpdate per a càlcul derivat: recapitulació amb criteri de cost

La 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à.

  1. 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ó.

  1. 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.

  1. Afinant 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.

  1. 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 tareasFiltradas a willUpdate sense comprovar abans si el cost real del filter sobre 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, willUpdate i evitar treball innecessari a render() 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(): si render() cridés this.tareasFiltradas dues 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 mateix render().

Exercicis

  1. Reescriu el getter tareasFiltradas de <task-list> perquè, en lloc d'executar-se a cada lectura, es recalculi dins de willUpdate únicament quan changedProperties inclogui tareas, 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 que tareas per si sola no capturaria).
  2. 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 atributs data-*, 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.
  3. 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.

  1. 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çó.
  2. 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

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