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
- La regla que governa el mòdul: mesurar primer
- El cost real de l'optimització prematura
- Què significa «lent» en una interfície: les mètriques que importen a l'usuari
- Per què es torna a executar un component
- El malentès més car de l'ecosistema
- Tècnica 1: baixar l'estat al component que el fa servir
- Tècnica 2: passar
childrenper aïllar un subarbre - Tècnica 3: derivar durant el render en lloc de desar i sincronitzar
- Tècnica 4: claus estables a les llistes
- Tècnica 5: retardar i limitar les entrades de text
- Tècnica 6: paginar i virtualitzar llistes llargues
- Tècnica 7: treure feina del fil principal
- El React Compiler de React 19: què automatitza i què no
- Pressupost de rendiment i flux de treball
- Mapa de la resta del mòdul
- 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
localhosti 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
StrictModeexecuta 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
setTimeoutde 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.
- 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.
- 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 | 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 | Sí: 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 | Sí: renders llargs, càlculs al render |
D'aquesta taula se n'extreuen dues conclusions que ordenen el mòdul sencer:
- INP i TBT són territori de React. Quan escriure al
CercadorBicicletestriga 400 ms a reflectir-se, això és INP, i s'arregla amb memoïtzació,useDeferredValueo virtualització (08-02, 08-03). - 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.
- 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"]
- 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 | Sí: es nota |
| Escriure al DOM real (crear, moure o esborrar nodes) | 0,5 – 5 ms per node | Sí, és el que és car |
| Provocar un recàlcul de maquetació (layout) | 5 – 50 ms | Sí, 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 | Sí: 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.
- 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.
- Tècnica 2: passar
children per aïllar un subarbre
children per aïllar un subarbreQuan 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
memosensememo, sense comparació i sense memòria addicional, només col·locant l'estat al lloc correcte de l'arbre.
- 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.
- 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.
- 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 | Sí |
| Cas típic | Cerca, desament automàtic, validació remota | Desplaçament, resize, arrossegament |
| A CicloUrbano | CercadorBicicletes |
useAmpladaFinestra en redimensionar |
- 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.
// 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:
countés el nombre lògic d'elements; el virtualitzador mai no els recorre tots.getScrollElementretorna l'element amboverflow: autoque genera el desplaçament.estimateSizepot ser aproximat: si les alçades varien, es corregeix mesurant ambmeasureElement.overscanmunta uns quants elements fora de la vista perquè el desplaçament ràpid no mostri buits.- El contenidor interior té l'alçada total real (2.000 × 120 px = 240.000 px) encara que estigui gairebé buit.
transform: translateY(...)és preferible atopperquè 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.
- 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.
- 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:
// 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 | Sí, l'equivalent a useMemo |
| Estabilitzar la identitat de funcions definides al component | Sí, l'equivalent a useCallback |
| Evitar reexecutar un component les props del qual no han canviat | Sí, l'equivalent a memo |
Estabilitzar el value d'un proveïdor de context |
Sí, 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,useMemoiuseCallbackescrits 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ó.
- 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,useCallbacki 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.
- 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:
- 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.
- 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.
- 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.
- 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.
- Elimina la documentació implícita. Un
useMemoamb dependències explícites comunica una intenció de disseny que el codi sense ell ja no expressa. - É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
- Què és React?
- Configuració de l'Entorn de Desenvolupament
- Hola Món amb React
- JSX: Extensió de Sintaxi de JavaScript
- Com Renderitza React: Virtual DOM i Reconciliació
Mòdul 2: Components de React
- Entendre els Components
- Components Funcionals vs de Classe
- Props: Passar Dades als Components
- State: Gestió de l'Estat del Component
- Estils en els Components: CSS, Mòduls i Utilitats
Mòdul 3: Treballar amb Esdeveniments
- Gestió d'Esdeveniments a React
- Renderitzat Condicional
- Llistes i Claus
- Formularis i Components Controlats
- Validació de Formularis i Components No Controlats
- Accessibilitat en Components Interactius
Mòdul 4: Conceptes Avançats de Components
- Elevar l'Estat
- Composició vs Herència
- Mètodes del Cicle de Vida de React
- Hooks: Introducció i Ús Bàsic
- Límits d'Error: Capturar Fallades a la Interfície
Mòdul 5: Hooks de React
- Hook useState
- Hook useEffect
- Hook useRef i Accés al DOM
- Hook useContext
- Hook useReducer
- Hooks Personalitzats
Mòdul 6: Enrutament a React
- Introducció a React Router
- Configuració de React Router
- Rutes Imbricades
- Navegació Programàtica
- Rutes Protegides i Control d'Accés
Mòdul 7: Gestió de l'Estat
- Introducció a la Gestió de l'Estat
- API de Context
- Redux: Introducció i Configuració
- Redux: Accions i Reductors
- Redux: Connectar-lo a React
- Estat del Servidor: Peticions, Memòria Cau i Sincronització
Mòdul 8: Optimització del Rendiment
- Tècniques d'Optimització del Rendiment a React
- Memoïtzació amb React.memo
- Hooks useMemo i useCallback
- Divisió de Codi i Càrrega Mandrosa
- Mesurar el Rendiment amb React DevTools Profiler
Mòdul 9: Proves a React
- Introducció a les Proves
- Proves Unitàries amb Jest
- Proves de Components amb React Testing Library
- Proves de Codi Asíncron i Simulació d'APIs
- Proves d'Extrem a Extrem amb Cypress
Mòdul 10: Temes Avançats
- Renderitzat al Servidor (SSR) amb Next.js
- Generació de Llocs Estàtics (SSG) amb Next.js
- Suspense i React Server Components
- TypeScript amb React
- React Native: Creació d'Aplicacions Mòbils
