El Mòdul 7 va acabar amb un diagnòstic incòmode: CicloUrbano sap on viu cada dada i per què, però no va ràpida. Hi ha components que es repinten sense motiu, càlculs que es refan a cada render i un paquet que el navegador descarrega sencer abans de mostrar la primera bicicleta. La temptació natural en arribar aquí és obrir l'editor i començar a repartir memo i useMemo pel projecte fins que «es noti millor». Aquesta temptació és exactament el que aquesta lliçó ve a desactivar. Optimitzar sense mesurar no és optimitzar: és afegir complexitat a cegues i esperar que surti bé. Abans de tocar ni una línia cal saber què significa «lent» per a l'usuari, per què es torna a executar un component, què és realment car i què no, i —el més important— que la majoria dels problemes de rendiment d'una aplicació React es resolen millor sense memoïtzar res. Aquesta lliçó és el panorama i el mètode: les tècniques que gairebé sempre rendeixen més que la memoïtzació, el paper del React Compiler de React 19 i el flux de treball que ordena tot el mòdul. Les eines de memoïtzació arriben a les lliçons següents, i arriben més ben preparades després d'això.

Contingut

  1. La regla que governa el mòdul: mesurar primer
  2. El cost real de l'optimització prematura
  3. Què significa «lent» en una interfície: les mètriques que importen a l'usuari
  4. Per què es torna a executar un component
  5. El malentès més car de l'ecosistema
  6. Tècnica 1: baixar l'estat al component que el fa servir
  7. Tècnica 2: passar children per aïllar un subarbre
  8. Tècnica 3: derivar durant el render en lloc de desar i sincronitzar
  9. Tècnica 4: claus estables a les llistes
  10. Tècnica 5: retardar i limitar les entrades de text
  11. Tècnica 6: paginar i virtualitzar llistes llargues
  12. Tècnica 7: treure feina del fil principal
  13. El React Compiler de React 19: què automatitza i què no
  14. Pressupost de rendiment i flux de treball
  15. Mapa de la resta del mòdul

  1. La regla que governa el mòdul: mesurar primer

No optimitzis el que no has mesurat. Si no pots assenyalar un número que empitjora, no tens un problema de rendiment: tens una sospita.

Aquesta regla sona a consell de manual i és una decisió d'enginyeria amb conseqüències molt concretes. Els motius pels quals la intuïció falla en rendiment web són sistemàtics:

  • La teva màquina no és la de l'usuari. Desenvolupes en un portàtil potent amb l'aplicació a localhost i sense latència de xarxa. L'usuari de CicloUrbano obre el catàleg en un mòbil de gamma mitjana, amb 4G irregular, mentre camina cap a l'estació.
  • El mode de desenvolupament menteix. El servidor de Vite serveix mòduls sense empaquetar, React registra avisos i informació de depuració, i StrictMode executa cada component dues vegades. Un render que en desenvolupament triga 18 ms pot trigar 4 ms en producció.
  • El que se sent lent i el que és lent rares vegades coincideixen. Un render de 300 ms en prémer un botó és una catàstrofe percebuda; la mateixa feina repartida en un setTimeout de fons no la nota ningú.
  • Els colls d'ampolla s'amaguen. El component que sospites gairebé mai és el culpable. A CicloUrbano, el culpable que escriure al cercador se senti pastós no és l'<input>: són les targetes que es repinten al darrere.

La conseqüència pràctica és que l'ordre de treball correcte comença sempre per la lliçó 08-05 (el Profiler), encara que sigui l'última del mòdul. Aquest mòdul s'estudia en ordre perquè cal conèixer les eines per entendre el que ensenya el Profiler, però s'aplica en l'ordre invers: mesurar, localitzar, corregir, tornar a mesurar.

  1. El cost real de l'optimització prematura

Quan algú diu que l'optimització prematura és cara, sol quedar-se en l'abstracte. Aquests són els costos reals, un a un.

Cost En què consisteix Exemple a CicloUrbano
Llegibilitat El codi deixa de dir el que fa i passa a dir com ho fa ràpid const alReservar = useCallback((id) => …, [usuari, mostrarAvis, crearReserva]) enfront d'una funció normal de tres línies
Memòria Cada valor memoïtzat es guarda. Memoïtzar 2.000 targetes és guardar 2.000 comparacions i 2.000 resultats Un memo en cada component fulla d'un catàleg llarg
Feina afegida Comparar props també costa. Si el component és barat, comparar surt més car que tornar-lo a executar memo sobre EtiquetaEstat, que només pinta un <span>
Errors per dependències Un array de dependències incomplet congela un valor obsolet; un d'excessiu anul·la la memoïtzació sense avisar Un useMemo que oblida ordre i continua mostrant el catàleg ordenat com estava
Falsa sensació de feina feta Es tanca l'assumpte sense haver tocat el problema real Memoïtzar tot el catàleg quan el que sobra són 900 KB de JavaScript inicial

El més greu, de bon tros, és el quart. Un memo innecessari alenteix una mica; un useMemo amb dependències mal posades produeix dades incorrectes en pantalla, i aquesta fallada és intermitent, difícil de reproduir i sol descobrir-la un usuari.

Un error de rendiment fa que l'aplicació vagi lenta. Un error de memoïtzació fa que l'aplicació menteixi.

  1. Què significa «lent» en una interfície: les mètriques que importen a l'usuari

«Lent» no és una sensació: és un conjunt de magnituds mesurables. Aquestes són les que la indústria ha consolidat (les anomenades Web Vitals), amb el que signifiquen i —dada clau per a aquest mòdul— quant pot fer React per cadascuna.

Mètrica Què mesura l'usuari Objectiu raonable Depèn de React?
FCP (First Contentful Paint, primer pintat amb contingut) Quant triga a aparèixer alguna cosa a la pantalla en blanc < 1,8 s Parcialment: sobretot de la mida del paquet (08-04) i del servidor
LCP (Largest Contentful Paint, pintat de l'element principal) Quant triga a veure's el contingut protagonista: la llista de bicicletes < 2,5 s Parcialment: paquet, dades i cost del primer render
TTI (Time To Interactive, temps fins a interactiu) Quan la pàgina respon de debò a un clic < 3,8 s : JavaScript descarregat, analitzat i executat
INP (Interaction to Next Paint, retard de la interacció) Quant triga la pantalla a reaccionar a un clic o a una tecla < 200 ms Sí, molt: és la mètrica d'aquest mòdul
CLS (Cumulative Layout Shift, estabilitat visual) Quant «salta» la maquetació mentre carrega < 0,1 : indicadors de càrrega i esquelets mal dissenyats (08-04)
TBT (Total Blocking Time, bloqueig del fil principal) Quant de temps el navegador no pot atendre l'usuari < 200 ms : renders llargs, càlculs al render

D'aquesta taula se n'extreuen dues conclusions que ordenen el mòdul sencer:

  1. INP i TBT són territori de React. Quan escriure al CercadorBicicletes triga 400 ms a reflectir-se, això és INP, i s'arregla amb memoïtzació, useDeferredValue o virtualització (08-02, 08-03).
  2. FCP i LCP són sobretot territori de l'empaquetador. Que el navegador hagi de descarregar 1,2 MB de JavaScript abans de pintar el catàleg no ho arregla cap memo: ho arregla la divisió de codi (08-04).

I una tercera, incòmoda: hi ha causes de lentitud que no són de React en absolut i cap tècnica d'aquest mòdul tocarà — imatges d'estacions sense comprimir ni dimensionar, una consulta a json-server que retorna les 2.000 bicicletes sense paginar, tipografies que bloquegen el pintat, o una petició en cascada que espera una altra. Abans de memoïtzar res, comprova que el problema és on creus.

  1. Per què es torna a executar un component

A 01-05 va quedar establert el cicle de React: render → diferències (diff) → confirmació (commit). Ara cal la part que aquell model deixava implícita: què fa que React torni a cridar la funció d'un component. Només hi ha quatre causes, i no n'hi ha una cinquena.

Causa Descripció Exemple
1. El seu propi estat canvia Un setEstat (o un dispatch) amb un valor diferent segons Object.is CercadorBicicletes en escriure una lletra
2. Un ancestre es torna a renderitzar React reexecuta el subarbre retornat pel pare, canviïn o no les props TargetaBicicleta quan PaginaCataleg es repinta
3. Canvia un context que consumeix Tot consumidor d'aquest context es reexecuta, encara que usi només una part del valor MenuUsuari quan canvia el tema, si comparteixen proveïdor
4. Canvia un valor subscrit d'un magatzem extern useSelector de Redux, useQuery de TanStack Query, useSyncExternalStore ResumFlota quan arriba una revalidació

Fixa't en el que no és a la llista: «que li hagin canviat les props». Les props no disparen res per si soles. Un component rep props noves perquè el seu pare s'ha tornat a executar (causa 2), i aquest és l'ordre causal correcte.

flowchart TD
    A["setEstat a PaginaCataleg"] --> B["React reexecuta<br/>PaginaCataleg"]
    B --> C["Reexecuta TOT el seu subarbre<br/>hagin canviat les props o no"]
    C --> D["CercadorBicicletes"]
    C --> E["SelectorTipus"]
    C --> F["LlistaBicicletes"]
    F --> G["TargetaBicicleta x N"]
    G --> H{"El JSX resultant<br/>difereix de l'anterior?"}
    H -->|No| I["React no toca el DOM<br/>cost: només la funcio JS"]
    H -->|Si| J["React aplica els canvis<br/>minims al DOM"]

  1. El malentès més car de l'ecosistema

Aquí hi ha la frase que cal interioritzar abans d'escriure ni un memo:

Que un component es torni a renderitzar no vol dir que el navegador torni a pintar res. Vol dir que React torna a cridar una funció de JavaScript i compara el resultat.

Un render és: executar la funció del component, obtenir un arbre d'objectes lleugers (elements de React), comparar-lo amb l'anterior i aplicar al DOM només el que ha canviat. Si TargetaBicicleta retorna exactament el mateix JSX que abans, el DOM no es toca. Això canvia completament l'escala del problema:

Operació Cost orientatiu Preocupant?
Executar la funció d'un component senzill 0,01 – 0,1 ms No
Comparar el seu arbre d'elements Proporcional al nombre de nodes, molt barat No
Executar-lo 2.000 vegades (catàleg complet) 20 – 200 ms : es nota
Escriure al DOM real (crear, moure o esborrar nodes) 0,5 – 5 ms per node , és el que és car
Provocar un recàlcul de maquetació (layout) 5 – 50 ms , molt car
Ordenar o filtrar 2.000 objectes 1 – 15 ms per render Depèn de amb quina freqüència passi
Formatar dates amb Intl 2.000 vegades 50 – 300 ms : Intl és sorprenentment car

La lectura correcta: un render de més no és un error, és una dada. Cent renders de components trivials poden ser irrellevants; un sol render que ordena un array de 2.000 elements i formata 2.000 dates pot arruïnar la interacció. Per això l'objectiu mai no és «reduir el nombre de renders», sinó reduir el temps de feina per interacció. Amb aquesta distinció clara, les set tècniques que vénen a continuació s'entenen soles: totes eliminen feina, no l'acceleren.

  1. Tècnica 1: baixar l'estat al component que el fa servir

És la tècnica més rendible de totes i no requereix cap API nova. Si un estat és més amunt d'on cal, cada canvi reexecuta un subarbre molt més gran del necessari. La solució és moure'l cap avall: és l'operació inversa a «elevar l'estat» de 04-01, i respon a la mateixa pregunta —qui necessita aquesta dada?— amb la resposta contrària.

// ❌ ABANS: l'esborrany del formulari viu a la pàgina
function PaginaNovaReserva() {
  const [hores, setHores] = useState(1);          // només el fa servir el formulari
  const { data: bicicletes } = useBicicletes();
  const { data: estacions } = useEstacions();

  return (
    <Panell titol="Nova reserva">
      <ResumFlota bicicletes={bicicletes} estacions={estacions} />
      <MollesDePa />
      <FormulariReserva hores={hores} alCanviarHores={setHores} />
    </Panell>
  );
}

Cada pulsació al camp d'hores reexecuta PaginaNovaReserva, i amb ella ResumFlota —que agrega les 2.000 bicicletes per estat— i MollesDePa. El camp escriu un número i el cost és un recompte complet de la flota.

// ✅ DESPRÉS: l'esborrany viu on es fa servir
function PaginaNovaReserva() {
  const { data: bicicletes } = useBicicletes();
  const { data: estacions } = useEstacions();

  return (
    <Panell titol="Nova reserva">
      <ResumFlota bicicletes={bicicletes} estacions={estacions} />
      <MollesDePa />
      <FormulariReserva />           {/* l'estat de les hores viu a dins */}
    </Panell>
  );
}

function FormulariReserva() {
  const [hores, setHores] = useState(1);   // el render s'atura aquí
  // …
}

Ara escriure al camp reexecuta un sol component. Sense memo, sense useCallback, sense dependències que mantenir. I el codi és més curt que abans, no més llarg: és el senyal que l'optimització és estructural i no un pedaç.

La regla operativa, que ja vas veure a 07-01 en classificar l'estat: col·loca cada estat en l'ancestre comú més baix de qui el llegeix, no més amunt.

  1. Tècnica 2: passar children per aïllar un subarbre

Quan l'estat no es pot baixar perquè el consumeix el mateix component que envolta la resta, queda la segona tècnica estructural, que ja va aparèixer a 04-02 i que aquí revela la seva veritable utilitat. El truc és en com funciona children: el JSX que es passa com a children es crea al component pare, no al que el rep. Si el pare no es reexecuta, aquests elements són literalment els mateixos objectes d'abans, i React es salta el seu render.

// ❌ ABANS: Disseny té l'estat del panell lateral i crea el contingut
function Disseny() {
  const [lateralObert, setLateralObert] = useState(false);

  return (
    <div className={estils.marc}>
      <Capcalera alAlternarLateral={() => setLateralObert((v) => !v)} />
      {lateralObert && <PanellLateral />}
      <main>
        <Outlet />       {/* tota la pàgina es reexecuta en obrir el lateral */}
      </main>
      <PeuDePagina />
    </div>
  );
}

Obrir el panell lateral reexecuta Disseny, i amb ell <Outlet /> i la pàgina completa que hi ha a sota: el catàleg amb les seves 2.000 targetes. Un canvi purament decoratiu del marc costa un render de tota l'aplicació.

// ✅ DESPRÉS: l'estat baixa a un component que rep children
function Disseny() {
  return (
    <MarcAmbLateral>
      <main>
        <Outlet />
      </main>
    </MarcAmbLateral>
  );
}

function MarcAmbLateral({ children }) {
  const [lateralObert, setLateralObert] = useState(false);

  return (
    <div className={estils.marc}>
      <Capcalera alAlternarLateral={() => setLateralObert((v) => !v)} />
      {lateralObert && <PanellLateral />}
      {children}          {/* creat per Disseny, que NO s'ha reexecutat */}
      <PeuDePagina />
    </div>
  );
}

Ara setLateralObert reexecuta només MarcAmbLateral. La prop children que rep és el mateix objecte que en el render anterior, React ho detecta per identitat i no baixa per aquesta branca. El catàleg ni se n'assabenta.

Aquesta tècnica s'anomena a vegades «memoïtzació estructural»: aconsegueix l'efecte de memo sense memo, sense comparació i sense memòria addicional, només col·locant l'estat al lloc correcte de l'arbre.

  1. Tècnica 3: derivar durant el render en lloc de desar i sincronitzar

La tercera tècnica no redueix renders: redueix la feina i elimina una classe sencera d'errors. És la mateixa lliçó de 05-02 sobre efectes innecessaris, llegida ara en clau de rendiment.

// ❌ Estat redundant sincronitzat amb un efecte
function ResumFlota({ bicicletes }) {
  const [disponibles, setDisponibles] = useState(0);

  useEffect(() => {
    setDisponibles(bicicletes.filter((b) => b.estat === 'disponible').length);
  }, [bicicletes]);

  return <p>{disponibles} bicicletes disponibles</p>;
}

Aquest component fa dos renders per cada canvi: un amb el valor vell i un altre després de l'efecte. A més, durant un instant mostra una dada incorrecta, i si bicicletes canvia d'identitat sense canviar de contingut, l'efecte es dispara igualment.

// ✅ Derivat durant el render: un sol render, sempre coherent
function ResumFlota({ bicicletes }) {
  const disponibles = bicicletes.filter((b) => b.estat === 'disponible').length;
  return <p>{disponibles} bicicletes disponibles</p>;
}

Un sol render, impossible de desincronitzar i menys codi. La pregunta que sorgeix immediatament —«i si el càlcul és car?»— té resposta a 08-03 amb useMemo, però l'ordre importa: primer es deriva, i només si es mesura que el càlcul pesa es memoïtza. Guardar en estat el que es pot calcular és un problema de correcció abans que de velocitat.

  1. Tècnica 4: claus estables a les llistes

A 03-03 va quedar establert per què key no és un adorn. Des de la perspectiva del rendiment, l'efecte d'una clau mal triada és brutal i silenciós.

// ❌ L'índex com a clau
{bicicletesVisibles.map((bicicleta, index) => (
  <TargetaBicicleta key={index} bicicleta={bicicleta} />
))}

// ❌ Encara pitjor: clau nova a cada render
{bicicletesVisibles.map((bicicleta) => (
  <TargetaBicicleta key={crypto.randomUUID()} bicicleta={bicicleta} />
))}

// ✅ Identitat real i estable de la dada
{bicicletesVisibles.map((bicicleta) => (
  <TargetaBicicleta key={bicicleta.id} bicicleta={bicicleta} />
))}
Clau Què passa en reordenar per preu Cost
key={index} React creu que tots els elements han canviat de contingut; actualitza props de tots i conserva l'estat a la posició equivocada Moltes escriptures al DOM + errors d'estat
key={crypto.randomUUID()} React destrueix i recrea tots els nodes a cada render Catastròfic: DOM complet + efectes remuntats
key={bicicleta.id} React mou els nodes existents Mínim, i l'estat viatja amb el seu element

Amb 2.000 targetes i l'ordre de sliceCataleg canviant de preu a model, la diferència entre la primera i la tercera opció és la diferència entre una animació fluida i un congelat de mig segon. I no costa res: és escriure la clau correcta.

  1. Tècnica 5: retardar i limitar les entrades de text

useDebounce ja és al projecte des de 05-06 i CercadorBicicletes el fa servir. Val la pena veure'l ara pel que és en realitat: una tècnica de rendiment que redueix la freqüència de la feina en lloc de reduir-ne el cost.

// src/components/CercadorBicicletes.jsx (recordatori)
function CercadorBicicletes() {
  const [text, setText] = useState('');
  const termeRetardat = useDebounce(text, 300);
  const despatxar = useDispatch();

  useEffect(() => {
    despatxar(termeCanviat(termeRetardat));
  }, [termeRetardat, despatxar]);

  return (
    <input
      type="search"
      value={text}                                  // el camp respon a cada tecla
      onChange={(e) => setText(e.target.value)}
      aria-label="Cercar bicicletes per model"
    />
  );
}

La clau del patró: l'<input> continua actualitzant-se a cada pulsació (és un component barat i la seva resposta ha de ser immediata), però el filtratge de les 2.000 bicicletes passa una sola vegada, 300 ms després de l'última tecla. De 12 filtratges en escriure «elèctrica» es passa a 1.

El seu parent proper és l'estrangulament (throttle), que en lloc d'esperar el silenci garanteix com a màxim una execució cada X mil·lisegons. És l'adequat per a esdeveniments continus que no tenen «final»: desplaçament, redimensionament, moviment del ratolí.

// src/hooks/useThrottle.js
import { useState, useEffect, useRef } from 'react';

export function useThrottle(valor, intervalMs = 200) {
  const [valorLimitat, setValorLimitat] = useState(valor);
  const ultimaExecucio = useRef(Date.now());

  useEffect(() => {
    const restant = intervalMs - (Date.now() - ultimaExecucio.current);

    if (restant <= 0) {
      ultimaExecucio.current = Date.now();
      setValorLimitat(valor);
      return;
    }
    const id = setTimeout(() => {
      ultimaExecucio.current = Date.now();
      setValorLimitat(valor);
    }, restant);

    return () => clearTimeout(id);
  }, [valor, intervalMs]);

  return valorLimitat;
}

Com triar entre els dos:

useDebounce useThrottle
Quan actua Quan l'usuari para A intervals regulars mentre passa
Garanteix execucions intermèdies No
Cas típic Cerca, desament automàtic, validació remota Desplaçament, resize, arrossegament
A CicloUrbano CercadorBicicletes useAmpladaFinestra en redimensionar

  1. Tècnica 6: paginar i virtualitzar llistes llargues

Suposem que CicloUrbano creix i db.json passa a tenir 2.000 bicicletes repartides per la ciutat — és l'escenari que aquest mòdul farà servir d'ara endavant. Pintar 2.000 TargetaBicicleta significa crear de l'ordre de 20.000 nodes del DOM. Cap memoïtzació arregla això, perquè la feina és real: el navegador ha de maquetar i pintar tot el que existeix.

Hi ha dues respostes, i la primera és gairebé sempre la millor:

Paginar. json-server ja admet _page i _limit, i a 07-06 es va preparar la consulta paginada amb placeholderData perquè la llista no parpellegi. Si l'usuari veu 24 bicicletes per pàgina, el problema desapareix d'arrel: mai no hi ha més de 24 targetes al DOM, i a més es descarreguen menys dades.

Virtualitzar (o «finestra lliscant»). Quan el disseny exigeix una llista contínua amb desplaçament infinit, la tècnica consisteix a muntar només els elements visibles més un petit marge, i simular l'alçada total amb un contenidor buit perquè la barra de desplaçament sigui creïble.

flowchart LR
    subgraph SIN["Sense virtualitzar"]
        A["2.000 targetes al DOM<br/>~20.000 nodes"]
    end
    subgraph CON["Virtualitzat"]
        B["Contenidor amb alçada total<br/>2.000 x 120px = 240.000px"]
        B --> C["Només ~14 targetes muntades<br/>les visibles + marge"]
        C --> D["En desplaçar: es reutilitzen<br/>canviant dades i posicio"]
    end

La biblioteca estàndard avui és @tanstack/react-virtual, dels mateixos autors que TanStack Query, sense dependències i agnòstica del marc de treball.

npm install @tanstack/react-virtual
// src/components/LlistaBicicletesVirtual.jsx
import { useRef } from 'react';
import { useVirtualizer } from '@tanstack/react-virtual';
import TargetaBicicleta from './TargetaBicicleta.jsx';
import estils from './LlistaBicicletesVirtual.module.css';

function LlistaBicicletesVirtual({ bicicletes }) {
  const contenidorRef = useRef(null);

  const virtualitzador = useVirtualizer({
    count: bicicletes.length,               // 1) quants elements hi ha en total
    getScrollElement: () => contenidorRef.current,  // 2) qui té el scroll
    estimateSize: () => 120,                // 3) alçada estimada de cada targeta, en px
    overscan: 5                             // 4) quants muntar fora de la vista
  });

  return (
    <div ref={contenidorRef} className={estils.finestra}>
      {/* 5) Un div amb l'alçada TOTAL: fa creïble la barra de desplaçament */}
      <div style={{ height: `${virtualitzador.getTotalSize()}px`, position: 'relative' }}>
        {virtualitzador.getVirtualItems().map((element) => {
          const bicicleta = bicicletes[element.index];
          return (
            // 6) Cada targeta es posiciona en absolut en el seu desplaçament real
            <div
              key={bicicleta.id}
              style={{
                position: 'absolute',
                top: 0,
                left: 0,
                width: '100%',
                height: `${element.size}px`,
                transform: `translateY(${element.start}px)`
              }}
            >
              <TargetaBicicleta bicicleta={bicicleta} />
            </div>
          );
        })}
      </div>
    </div>
  );
}

export default LlistaBicicletesVirtual;

Punt per punt:

  1. count és el nombre lògic d'elements; el virtualitzador mai no els recorre tots.
  2. getScrollElement retorna l'element amb overflow: auto que genera el desplaçament.
  3. estimateSize pot ser aproximat: si les alçades varien, es corregeix mesurant amb measureElement.
  4. overscan munta uns quants elements fora de la vista perquè el desplaçament ràpid no mostri buits.
  5. El contenidor interior té l'alçada total real (2.000 × 120 px = 240.000 px) encara que estigui gairebé buit.
  6. transform: translateY(...) és preferible a top perquè no provoca recàlcul de maquetació.

El resultat en xifres: de ~20.000 nodes del DOM a ~140. Cap altra tècnica d'aquest mòdul s'acosta a aquesta millora. Abans de memoïtzar una llista llarga, pregunta't si hauria de ser curta.

Advertiments honestos sobre la virtualització, perquè no és gratis: trenca la cerca del navegador (Ctrl+F no troba el que no està muntat), complica l'accessibilitat i el focus per teclat, exigeix cura amb les alçades variables i interfereix amb la impressió. Per això paginar sol ser millor decisió de producte.

  1. Tècnica 7: treure feina del fil principal

El navegador té un sol fil per executar JavaScript, respondre a esdeveniments i pintar. Tot el que ocupi aquest fil més de ~50 ms es converteix en una interfície que no respon. Les palanques, de més simple a més complexa:

Palanca Què fa Quan usar-la a CicloUrbano
Fer menys Paginar, filtrar al servidor, demanar menys camps json-server amb _page, _limit i _sort en lloc de portar 2.000 bicicletes
Fer-ho un cop Precalcular i guardar a la memòria cau de Query El recompte per estació, calculat a select de la consulta
Fer-ho més tard useTransition / useDeferredValue (08-03) Filtrar el catàleg mentre el camp continua responent
Fer-ho fora Web Worker Un informe d'ús mensual sobre milers de reserves
Que ho faci el CSS Animar amb transform i opacity, que no toquen la maquetació Transició d'obertura del Modal
Que ho faci el navegador content-visibility: auto, loading="lazy" a les imatges Fotos d'estacions sota el plec

Un exemple de l'últim cas, que sovint es passa per alt perquè no és «codi React»:

/* src/components/LlistaBicicletes.module.css */
.targeta {
  content-visibility: auto;       /* el navegador no maqueta el que no es veu */
  contain-intrinsic-size: 0 120px; /* alçada reservada perquè el scroll no salti */
}

Dues línies de CSS que eviten que el navegador maqueti targetes fora de la pantalla. No és virtualització —els nodes continuen existint al DOM— però el cost de maquetació i pintat baixa molt, i no requereix cap biblioteca.

  1. El React Compiler de React 19: què automatitza i què no

React 19 va estabilitzar el React Compiler, i convé explicar-lo amb precisió perquè genera dos malentesos oposats: qui creu que ja no cal aprendre memoïtzació, i qui l'ignora completament.

Què és. Un compilador que s'executa en temps de construcció (com un complement de Babel dins de Vite), analitza els teus components i hooks, i insereix automàticament la memoïtzació que tu hauries escrit a mà amb memo, useMemo i useCallback. Converteix el codi en una versió equivalent que reutilitza valors, funcions i elements JSX quan les seves entrades no han canviat.

Com s'activa a Vite:

npm install --save-dev babel-plugin-react-compiler
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [
    react({
      babel: {
        plugins: [['babel-plugin-react-compiler', { target: '19' }]]
      }
    })
  ]
});

I la regla d'or per adoptar-lo amb cap: abans d'activar-lo, passa el linter de les regles de React (eslint-plugin-react-hooks inclou les regles del compilador). El compilador és conservador: si detecta que un component trenca les regles de React —muta props, escriu en una variable de mòdul durant el render, llegeix ref.current mentre renderitza— es salta aquest component i el deixa sense optimitzar, en silenci. Un projecte amb moltes infraccions obté moltes menys millores de les que espera.

Què automatitza i què no:

Feina Ho fa el compilador?
Memoïtzar el resultat d'un càlcul entre renders , l'equivalent a useMemo
Estabilitzar la identitat de funcions definides al component , l'equivalent a useCallback
Evitar reexecutar un component les props del qual no han canviat , l'equivalent a memo
Estabilitzar el value d'un proveïdor de context , si el valor es construeix al mateix component
Reduir la mida del paquet JavaScript No. És cosa de 08-04
Evitar un càlcul que és car per si mateix en la seva primera execució No. Ordenar 2.000 bicicletes continua costant el que costa
Saber que una dependència externa (una funció importada, un objecte d'una biblioteca) és estable No. No pot raonar més enllà del teu component
Arreglar un useEffect que es dispara de més No, i continua sent responsabilitat teva
Optimitzar codi que trenca les regles de React No: l'omet sencer
Decidir baixar l'estat, passar children, paginar o virtualitzar No. Cap decisió estructural d'aquesta lliçó

Per què aquest mòdul continua sent imprescindible, encara que activis el compilador demà:

  • Per llegir codi existent. La immensa majoria de projectes React en producció estan plens de memo, useMemo i useCallback escrits a mà. Sense entendre'ls no els pots mantenir.
  • Per depurar. Quan alguna cosa va malament amb el compilador activat, el diagnòstic exigeix saber exactament quina memoïtzació s'esperava i quina no va arribar.
  • Perquè molts projectes no l'adoptaran. Bases de codi grans, versions antigues de React, cadenes de construcció que no usen Babel.
  • Perquè no elimina el criteri. El compilador memoïtza; no decideix què val la pena calcular, què hauria d'estar paginat ni on ha de viure l'estat. Aquesta continua sent la feina difícil.
  • Perquè memoïtzar no és optimitzar. Si el problema és que descarregues 1,2 MB de JavaScript o que demanes 2.000 registres, el compilador no ajuda gens.

El React Compiler elimina la feina mecànica de memoïtzar. No elimina la necessitat d'entendre què es memoïtza, per què i quan això no és la solució.

  1. Pressupost de rendiment i flux de treball

Un pressupost de rendiment converteix «això va lent» en una condició verificable. Sense ell no hi ha manera de saber quan cal optimitzar ni, sobretot, quan cal parar. Aquest és un pressupost raonable per a CicloUrbano:

Magnitud Pressupost Com es mesura
JavaScript inicial (comprimit) < 200 KB Sortida de npm run build (08-04)
LCP en mòbil de gamma mitjana < 2,5 s Lighthouse en mode mòbil
INP en escriure al cercador < 200 ms Profiler + pestanya Rendiment (08-05)
Durada del commit en filtrar el catàleg < 16 ms React DevTools Profiler (08-05)
Targetes muntades simultàniament < 60 Inspector d'elements

I aquest és el flux de treball que ordena tot el mòdul. Llegeix-lo com un bucle, no com una llista:

flowchart TD
    A["1. Definir el pressupost<br/>i la interaccio concreta"] --> B["2. MESURAR en build de produccio<br/>Profiler + Lighthouse"]
    B --> C{"S'incompleix<br/>el pressupost?"}
    C -->|No| Z["PARAR<br/>no hi ha res a optimitzar"]
    C -->|Si| D["3. Localitzar el coll d'ampolla<br/>quin component, quin commit, per que"]
    D --> E{"Quin tipus de<br/>problema es?"}
    E -->|"Descarrega inicial"| F["Divisio de codi 08-04"]
    E -->|"Massa elements"| G["Paginar / virtualitzar"]
    E -->|"Estat mal col·locat"| H["Baixar estat / children"]
    E -->|"Frequencia excessiva"| I["Debounce / transicions"]
    E -->|"Renders amb props iguals"| J["memo 08-02"]
    E -->|"Calcul car o identitat"| K["useMemo / useCallback 08-03"]
    F --> L["4. Aplicar la tecnica MES SIMPLE<br/>una sola cada vegada"]
    G --> L
    H --> L
    I --> L
    J --> L
    K --> L
    L --> M["5. TORNAR A MESURAR"]
    M --> N{"Ha millorat de forma<br/>perceptible?"}
    N -->|Si| B
    N -->|No| O["Revertir el canvi<br/>i tornar al pas 3"]
    O --> D

Dos detalls del diagrama que no són decoratius:

  • «Aplicar una sola tècnica cada vegada». Si apliques memo, useCallback i virtualització alhora i millora, no saps quina ha servit; probablement estiguis carregant amb dues optimitzacions inútils per sempre.
  • «Revertir el canvi». Una optimització que no millora de forma mesurable s'ha de desfer. El seu cost de llegibilitat i memòria continua allà encara que el benefici no existeixi.

  1. Mapa de la resta del mòdul

Lliçó Eina Problema que resol
08-02 React.memo Un component es reexecuta encara que les seves props siguin idèntiques
08-03 useMemo, useCallback, useTransition, useDeferredValue Un càlcul és car, o un valor canvia d'identitat i espatlla tota la resta
08-04 React.lazy, Suspense, import() El navegador descarrega codi que encara no necessita
08-05 React DevTools Profiler, <Profiler> Saber què està passant de debò, que és el pas 2 de tots els anteriors

Errors Comuns i Consells

Optimitzar en mode de desenvolupament. El servidor de Vite, els avisos de React i el doble render de StrictMode distorsionen qualsevol mesurament. Tot el que es mesuri de debò es mesura amb npm run build i npm run preview (08-05).

Perseguir el nombre de renders com a objectiu. «He baixat de 340 renders a 12» no és un èxit si la interacció trigava 30 ms i continua trigant 28. La mètrica és el temps, no el recompte.

Començar per la memoïtzació. És l'ordre invertit. Quan s'arriba a memo havent descartat baixar l'estat, children, derivar, paginar i virtualitzar, el memo sol sobrar. Quan es comença per memo, gairebé sempre s'acaba amb un projecte ple de memoïtzació i el problema real intacte.

Consell: mesura en un dispositiu lent. Les eines del navegador permeten alentir la CPU 4× o 6× (CPU throttling). Un problema que al teu portàtil és invisible es torna obvi amb 4×, que és aproximadament un mòbil de gamma mitjana.

Consell: posa el problema a la seva capa. Abans de tocar React, comprova la mida de les imatges, el nombre de peticions, si el servidor pagina i si les tipografies bloquegen el pintat. Moltes «aplicacions React lentes» són aplicacions amb 3 MB d'imatges.

Consell: escriu el pressupost al repositori. Un fitxer RENDIMENT.md amb les cinc xifres de l'apartat 14 converteix una discussió d'opinions en una comprovació objectiva, i sobreviu als canvis d'equip.

Consell: activa les regles del linter abans que el compilador. eslint-plugin-react-hooks amb les regles del React Compiler et diu quins components trenquen les regles de React. Arreglar-los millora el codi encara que mai no activis el compilador, i és requisit perquè aquest serveixi d'alguna cosa.

Exercicis

Exercici 1. El component següent de CicloUrbano fa que escriure al camp de notes repinti tot el panell de reserves, inclòs PanellReserves, que agrega l'historial complet de l'usuari. Identifica el problema, digues quina tècnica d'aquesta lliçó el resol i reescriu el component.

function PaginaReserves() {
  const [nota, setNota] = useState('');
  const { data: reserves } = useReserves();

  return (
    <Panell titol="Les meves reserves">
      <PanellReserves reserves={reserves} />
      <ResumFlota />
      <label>
        Nota interna
        <input value={nota} onChange={(e) => setNota(e.target.value)} />
      </label>
      <button onClick={() => desarNota(nota)}>Desar nota</button>
    </Panell>
  );
}

Exercici 2. Per a cada símptoma de CicloUrbano, indica quina mètrica de l'apartat 3 empitjora i quina tècnica d'aquesta lliçó (o quina lliçó del mòdul) és l'adequada. No usis memo, useMemo ni useCallback en cap resposta.

# Símptoma
a La primera visita triga 4,1 s a mostrar el catàleg en 4G
b En canviar l'ordre del catàleg de preu a model, la pantalla es congela 600 ms
c Cada tecla al cercador provoca una petició a json-server
d En acabar de carregar les fotos d'estacions, la llista fa un salt i l'usuari prem on no volia
e Desplaçar-se per les 2.000 bicicletes va a batzegades al mòbil

Exercici 3. Un company proposa activar el React Compiler i esborrar tots els memo, useMemo i useCallback del projecte «perquè ara són automàtics». Enumera almenys quatre objeccions tècniques concretes i descriu quina comprovació faries abans d'acceptar o rebutjar la proposta.

Solucions

Solució 1. El problema és estat mal col·locat: nota només el fan servir l'<input> i el botó, però viu a PaginaReserves, així que cada pulsació reexecuta PanellReserves i ResumFlota. La tècnica és la 1, baixar l'estat (apartat 6), extraient el camp i el seu botó a un component propi.

function PaginaReserves() {
  const { data: reserves } = useReserves();

  return (
    <Panell titol="Les meves reserves">
      <PanellReserves reserves={reserves} />
      <ResumFlota />
      <CampNotaInterna />        {/* l'estat s'atura aquí */}
    </Panell>
  );
}

function CampNotaInterna() {
  const [nota, setNota] = useState('');

  return (
    <>
      <label>
        Nota interna
        <input value={nota} onChange={(e) => setNota(e.target.value)} />
      </label>
      <button onClick={() => desarNota(nota)}>Desar nota</button>
    </>
  );
}

Ara escriure reexecuta només CampNotaInterna. No cal memo a PanellReserves ni a ResumFlota: React ni tan sols baixa per aquesta branca. És la lliçó estructural del mòdul: la millor optimització és la que fa innecessària l'optimització.

Solució 2.

# Mètrica Tècnica
a LCP / FCP (i TTI) Divisió de codi i càrrega mandrosa (08-04), a més de paginar la consulta inicial
b INP / TBT Comprovar primer les claus estables (tècnica 4): amb key={index} una reordenació reescriu el DOM sencer. Després, paginar o virtualitzar (tècnica 6) i, si cal, useTransition (08-03)
c INP i càrrega del servidor useDebounce (tècnica 5), ja present a CercadorBicicletes; combinat amb el staleTime de la consulta
d CLS Reservar l'espai: width/height o aspect-ratio a les imatges, i esquelets amb l'alçada final en lloc d'un text «Carregant…» (es retoma a 08-04)
e INP / TBT i memòria Paginar o virtualitzar (tècnica 6) amb @tanstack/react-virtual, i content-visibility: auto com a mesura immediata (tècnica 7)

Cap de les cinc es resol memoïtzant. Aquest és exactament l'objectiu de l'exercici.

Solució 3. Objeccions:

  1. El compilador no memoïtza el que no pot analitzar. Si un component trenca les regles de React, l'omet en silenci; en esborrar la memoïtzació manual, aquests components queden pitjor que abans i sense cap avís.
  2. No cobreix les dependències externes. Una funció importada d'una utilitat o un objecte d'opcions creat fora del component continuen necessitant criteri humà; el compilador raona dins del component.
  3. No abarateix els càlculs cars. Ordenar i filtrar 2.000 bicicletes costa el mateix la primera vegada; el compilador evita repetir-ho, no evita fer-ho.
  4. Trenca el codi per a qui no el tingui activat. Si part del projecte s'extreu a una biblioteca compartida, o si un altre equip compila sense el complement, la memoïtzació desapareix.
  5. Elimina la documentació implícita. Un useMemo amb dependències explícites comunica una intenció de disseny que el codi sense ell ja no expressa.
  6. És un canvi irreversible a la pràctica. Tornar a introduir la memoïtzació manual en centenars de components costa molt més que deixar-la.

Comprovació prèvia: mesurar abans i després amb el Profiler (08-05) sobre les interaccions crítiques —escriure al cercador, canviar l'ordre, obrir el DialegReserva— en un build de producció, amb i sense el compilador, i amb el linter de regles de React en verd. Si les xifres són equivalents, el compilador està fent la seva feina i la retirada de memoïtzació manual es pot plantejar gradualment, component a component, no en una sola operació.

Conclusió

Aquest mòdul comença pel mètode perquè sense mètode l'optimització és superstició. La regla que ho governa tot és mesurar primer: si no pots assenyalar un número que incompleix un pressupost, no tens un problema, tens una sospita, i actuar sobre sospites costa llegibilitat, memòria i —el més greu— errors de dependències que fan que l'aplicació mostri dades incorrectes.

«Lent» s'ha convertit en magnituds concretes: FCP i LCP depenen sobretot de la mida del paquet i del servidor; INP i TBT són el territori propi de React i d'aquest mòdul; CLS s'arruïna amb indicadors de càrrega mal dissenyats. I s'ha fixat el model causal: un component es torna a executar per quatre raons —el seu estat, un ancestre, un context que consumeix, un magatzem extern subscrit—, entre les quals no hi és «li han canviat les props». D'aquí el malentès més car de l'ecosistema, ja desactivat: tornar a renderitzar no és tornar a pintar. Un render és executar una funció i comparar el resultat; el DOM només es toca on ha canviat alguna cosa. El que és car no són els renders, és la feina per interacció.

Amb això clar, les set tècniques que gairebé sempre rendeixen més que la memoïtzació: baixar l'estat al component que el fa servir (la més rendible de totes, i a més escurça el codi), passar children perquè un subarbre conservi la seva identitat i React no baixi per aquesta branca, derivar durant el render en lloc de guardar i sincronitzar amb un efecte, claus estables a les llistes —on un key={index} converteix una reordenació en una reescriptura completa del DOM—, useDebounce i useThrottle per reduir la freqüència de la feina, paginar o virtualitzar amb @tanstack/react-virtual quan el catàleg creix a 2.000 bicicletes, i treure feina del fil principal amb precàlcul, CSS i content-visibility. Cap no necessita memo.

El React Compiler de React 19 s'ha presentat sense exageracions: automatitza la memoïtzació mecànica que hauries escrit a mà, no redueix la mida del paquet, no abarateix un càlcul car, no raona sobre dependències externes, omet en silenci el codi que trenca les regles de React i no pren cap decisió estructural. Continua fent falta entendre la memoïtzació manual per llegir el codi que existeix, per depurar i per als projectes que no l'adoptin. I el flux de treball que ordena el mòdul: mesurar → localitzar → aplicar la tècnica més simple, una de sola cada vegada → tornar a mesurar → revertir si no millora.

Ara sí que arriba el torn de les eines. La primera és la que respon a la causa 2 de la llista —«un ancestre s'ha tornat a renderitzar»— quan ja no queda marge estructural per evitar-ho: embolicar un component perquè React compari les seves props i es salti la seva execució si són les mateixes. La propera lliçó és Memoïtzació amb React.memo.

Curs de React

Mòdul 1: Introducció a React

Mòdul 2: Components de React

Mòdul 3: Treballar amb Esdeveniments

Mòdul 4: Conceptes Avançats de Components

Mòdul 5: Hooks de React

Mòdul 6: Enrutament a React

Mòdul 7: Gestió de l'Estat

Mòdul 8: Optimització del Rendiment

Mòdul 9: Proves a React

Mòdul 10: Temes Avançats

Mòdul 11: Projecte: Construir una Aplicació Completa

© Copyright 2026. Tots els drets reservats