Les dues lliçons anteriors han atacat el treball que fa React mentre l'usuari usa l'aplicació: renders que sobren, càlculs que es repeteixen, identitats que canvien sense motiu. Queda l'altre problema, el que apareix abans que l'usuari pugui usar res. Avui, quan algú obre CicloUrbano per veure si queda una bicicleta a Plaça Major, el navegador descarrega i executa el codi del panell de taller, el del detall d'estacions amb les seves pestanyes, el del formulari de reserva i la biblioteca de gràfiques d'ocupació, abans de pintar la primera targeta. Tot aquest codi és correcte, està ben memoïtzat i no es farà servir en aquesta visita. Cap tècnica de memoïtzació ho arregla, perquè el problema no és quantes vegades s'executa alguna cosa: és quant pesa el que es descarrega. La solució es diu divisió de codi, consisteix a partir el paquet en fragments que es descarreguen quan calen, i a React s'expressa amb dues peces —React.lazy i <Suspense>— sobre un mecanisme del propi JavaScript: l'import() dinàmic. En aquesta lliçó aprendràs a mesurar el paquet, a partir-lo per ruta i per component, a precarregar perquè l'espera no es noti, a dissenyar l'espera sense salts visuals i a sobreviure quan un fragment no arriba.

Contingut

  1. El problema del paquet únic
  2. Mesurar abans de partir: inspeccionar el paquet
  3. Què és la divisió de codi i què són els fragments
  4. L'import() dinàmic com a frontera automàtica
  5. React.lazy i <Suspense fallback>
  6. Divisió per ruta: la que més rendeix
  7. Per què el catàleg no es divideix
  8. Divisió per component
  9. Precàrrega: que l'espera no es noti
  10. Dissenyar l'espera: esquelets en lloc de «Carregant…»
  11. Quan el fragment no arriba
  12. Altres palanques de mida
  13. Suspense és molt més que això

  1. El problema del paquet únic

Quan executes npm run build, Vite recorre el graf d'importacions des de main.jsx, resol cada import estàtic i ho empaqueta tot junt. Un fitxer, o uns pocs. El navegador ha de descarregar-lo sencer, analitzar-lo sencer i executar-lo sencer abans que React pinti res.

Aquestes són les dependències reals de CicloUrbano després del mòdul 7, amb mides orientatives de codi d'aplicació ja minificat i comprimit amb gzip:

Part Sense comprimir Comprimit (gzip)
react + react-dom ~140 KB ~45 KB
react-router ~70 KB ~22 KB
@reduxjs/toolkit + react-redux ~90 KB ~28 KB
@tanstack/react-query ~110 KB ~35 KB
Codi propi de CicloUrbano ~180 KB ~45 KB
Biblioteca de gràfiques d'ocupació ~320 KB ~95 KB
Total ~910 KB ~270 KB

I ara la part incòmoda: d'aquests 270 KB comprimits, la ruta d'entrada en fa servir aproximadament la meitat. Les gràfiques només apareixen al detall d'estació, PaginaTaller només la veu un operari, i PaginaNovaReserva només s'obre en fer una reserva.

El cost no és només la descàrrega. Analitzar i executar JavaScript és treball de CPU, i en un mòbil de gamma mitjana va entre 3 i 5 vegades més lent que al teu portàtil:

flowchart LR
    A["Descarregar<br/>270 KB en 4G<br/>~1,4 s"] --> B["Analitzar i compilar<br/>910 KB sense comprimir<br/>~0,6 s al mobil"]
    B --> C["Executar<br/>moduls + main.jsx<br/>~0,3 s"]
    C --> D["Primer render<br/>+ peticio a l'API"]
    D --> E["Cataleg visible<br/>~3,5 s"]

Tres segons i mig fins a veure una bicicleta, la meitat gastats en codi que aquesta visita no farà servir. Aquest és el problema exacte que resol aquesta lliçó.

  1. Mesurar abans de partir: inspeccionar el paquet

Igual que a tot el mòdul: primer es mesura. Vite ja dona la primera mesura sense instal·lar res.

npm run build
vite v6.0.5 building for production...
✓ 412 modules transformed.
dist/index.html                     0.48 kB │ gzip:   0.31 kB
dist/assets/index-B4nQ8xJ2.css     18.24 kB │ gzip:   4.02 kB
dist/assets/index-DvK2mR7p.js     908.77 kB │ gzip: 271.35 kB

(!) Some chunks are larger than 500 kB after minification. Consider:
- Using dynamic import() to code-split the application

Aquest avís de Rollup no és decoratiu: és exactament el consell d'aquesta lliçó. Però la sortida de Vite diu quant pesa, no què pesa. Per a això cal un mapa del contingut.

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

export default defineConfig({
  plugins: [
    react(),
    visualizer({
      filename: 'dist/estadistiques.html',
      gzipSize: true,        // mostra la mida comprimida, que és la que importa
      brotliSize: true,
      open: false            // posa-ho a true per obrir-lo automàticament
    })
  ]
});

Després de npm run build, obre dist/estadistiques.html: un mapa d'arbre on cada rectangle és un mòdul i la seva superfície és el seu pes. El que cal buscar, per ordre:

Senyal al mapa Què significa Acció
Un rectangle enorme d'una biblioteca Una dependència pesada que potser no es fa servir sempre Carregar-la sota demanda (apartat 8)
Tot el teu codi en un sol bloc No hi ha cap divisió Dividir per ruta (apartat 6)
Una biblioteca de la qual només uses una funció Importació no selectiva o biblioteca sense tree-shaking Revisar la importació (apartat 12)
Dades .json grans empaquetades Dades que s'haurien de demanar, no incrustar import() dinàmic o petició
moment, lodash sencers, icones senceres Els sospitosos habituals Substituir o importar selectivament

Anota la xifra de partida —271 KB comprimits— perquè l'apartat 6 la compara amb la del després. Sense aquest abans i després, això no és optimització.

  1. Què és la divisió de codi i què són els fragments

Dividir el codi (code splitting) és partir el paquet únic en diversos fitxers, anomenats fragments (chunks), de manera que el navegador descarregui només els que necessita per al que l'usuari està fent.

La divisió té dos nivells, i convé distingir-los:

  • Divisió automàtica per dependències comunes. Vite ja separa node_modules en un fragment de proveïdors (vendor) perquè el codi de biblioteques, que canvia poc, es cachegi millor entre desplegaments. Això passa sol, i no afecta quant es descarrega a la primera visita.
  • Divisió per punts de càrrega, que és la d'aquesta lliçó: tu decideixes quines parts de la teva aplicació poden esperar i les marques com a tals.

L'interessant és com es marquen: no hi ha cap configuració. La frontera entre fragments la crea una sintaxi del propi JavaScript.

  1. L'import() dinàmic com a frontera automàtica

// Importació estàtica: es resol en construir, entra al paquet principal
import { generarInforme } from './utilitats/informes.js';

// Importació dinàmica: retorna una promesa, i crea un FRAGMENT NOU
const { generarInforme } = await import('./utilitats/informes.js');

Diferències essencials:

import estàtic import() dinàmic
Quan es resol En construir el paquet En temps d'execució
Què retorna Enllaços (bindings) Una promesa de l'objecte de mòdul
On pot aparèixer Només al nivell superior del mòdul A qualsevol lloc: dins d'una funció, d'un if
Efecte en l'empaquetat S'incorpora al fragment actual Crea un fragment separat
Pot portar variables No Sí, amb limitacions (Vite necessita pistes estàtiques a la ruta)

I l'efecte al graf de mòduls:

flowchart TD
    subgraph ABANS["Un sol fragment"]
        M1["main.jsx"] --> R1["rutes.jsx"]
        R1 --> P1["PaginaCataleg"]
        R1 --> P2["PaginaTaller"]
        R1 --> P3["PaginaDetallEstacio"]
        P3 --> G1["Biblioteca de grafiques<br/>95 KB"]
        R1 --> P4["PaginaNovaReserva"]
        M1 --> V1["react, router, redux, query"]
    end
flowchart TD
    subgraph DESPRES["Fragment inicial + fragments sota demanda"]
        M2["main.jsx"] --> R2["rutes.jsx"]
        R2 --> P5["PaginaCataleg<br/>ruta d'entrada: estatica"]
        M2 --> V2["react, router, redux, query"]
        R2 -.->|"import() en navegar"| C1["chunk: PaginaTaller"]
        R2 -.->|"import() en navegar"| C2["chunk: PaginaDetallEstacio"]
        C2 -.->|"import() en obrir pestanya"| C3["chunk: grafiques 95 KB"]
        R2 -.->|"import() en navegar"| C4["chunk: PaginaNovaReserva"]
    end

Les línies discontínues són peticions de xarxa que passen quan l'usuari hi arriba, no abans. I no cal configurar res: escriure import(...) és la instrucció completa.

  1. React.lazy i <Suspense fallback>

import() retorna una promesa, i JSX no sap què fer amb una promesa. React.lazy és el pont.

import { lazy, Suspense } from 'react';

const PaginaTaller = lazy(() => import('./pagines/PaginaTaller.jsx'));

<Suspense fallback={<IndicadorDeCarrega missatge="Carregant taller…" />}>
  <PaginaTaller />
</Suspense>

Signatura i exigències de lazy:

  • Rep una funció que retorna una promesa d'un mòdul. No la promesa directament: la funció permet a React cridar-la en el moment adequat.
  • El mòdul ha d'exposar el component com a exportació per defecte. És el que React llegeix de la promesa resolta. La convenció de CicloUrbano (export default per als components) hi encaixa sense canvis.
  • lazy() s'ha de cridar fora de qualsevol component, en l'àmbit del mòdul. Si es crida a dins, cada render crearia un component mandrós nou i desmuntaria l'anterior, amb la qual cosa la pantalla parpellejaria sense fi.

Si un mòdul fes servir exportació amb nom, l'adaptació és una línia:

// El mòdul exporta { PanellGrafiques }, no un default
const PanellGrafiques = lazy(() =>
  import('./components/PanellGrafiques.jsx').then((modul) => ({ default: modul.PanellGrafiques }))
);

Comportament la primera vegada enfront de les següents:

Moment Què passa
Primera vegada que es renderitza React crida la funció, es dispara la petició del fragment, el component «suspèn» i Suspense mostra el fallback
Quan la promesa es resol React renderitza el component real al lloc del fallback
Vegades següents El mòdul ja és en memòria: no hi ha fallback ni parpelleig. És instantani
Si la promesa es rebutja L'error puja fins al límit d'error més proper (apartat 11)

<Suspense> té una sola prop rellevant aquí, fallback, i una regla de col·locació: ha d'estar per sobre del component mandrós a l'arbre, i la seva posició decideix quina part de la interfície se substitueix per l'indicador de càrrega. Un Suspense a l'arrel substitueix tota l'aplicació; un al voltant del <Outlet /> substitueix només el contingut de la pàgina i deixa la Capcalera i el PeuDePagina intactes. Aquesta diferència es nota moltíssim.

  1. Divisió per ruta: la que més rendeix

La divisió per ruta és la que té millor relació benefici/esforç, i el motiu és evident quan es diu en veu alta: l'usuari és en una ruta a la vegada. Tot el que pertany a altres rutes és, per definició, codi que ara no fa falta.

Aquest és el mapa de rutes de CicloUrbano amb la divisió aplicada:

// src/rutes.jsx
import { lazy, Suspense } from 'react';
import { createBrowserRouter } from 'react-router';

import Disseny from './components/Disseny.jsx';
import PaginaCataleg from './pagines/PaginaCataleg.jsx';          // 1) ruta d'entrada: estàtica
import PaginaAcces from './pagines/PaginaAcces.jsx';              // 2) petita i molt usada
import PaginaNoTrobada from './pagines/PaginaNoTrobada.jsx';
import PaginaSensePermisos from './pagines/PaginaSensePermisos.jsx';
import PaginaErrorRuta from './pagines/PaginaErrorRuta.jsx';      // 3) MAI mandrosa
import RutaProtegida from './components/RutaProtegida.jsx';
import RequereixRol from './components/RequereixRol.jsx';
import EsqueletPagina from './components/EsqueletPagina.jsx';
import { estacions } from './dades/domini.js';

// 4) Fragments sota demanda: un per cada import() dinàmic
const PaginaFitxaBicicleta = lazy(() => import('./pagines/PaginaFitxaBicicleta.jsx'));
const PaginaEstacions      = lazy(() => import('./pagines/PaginaEstacions.jsx'));
const PaginaDetallEstacio  = lazy(() => import('./pagines/PaginaDetallEstacio.jsx'));
const PestanyaFlota        = lazy(() => import('./pagines/PestanyaFlota.jsx'));
const PestanyaIncidencies  = lazy(() => import('./pagines/PestanyaIncidencies.jsx'));
const PaginaReserves       = lazy(() => import('./pagines/PaginaReserves.jsx'));
const PaginaNovaReserva    = lazy(() => import('./pagines/PaginaNovaReserva.jsx'));
const PaginaTaller         = lazy(() => import('./pagines/PaginaTaller.jsx'));

// 5) Embolcall reutilitzable: cada pàgina mandrosa amb el seu propi Suspense
function Mandrosa({ children }) {
  return <Suspense fallback={<EsqueletPagina />}>{children}</Suspense>;
}

export const router = createBrowserRouter([
  {
    path: '/',
    element: <Disseny />,
    errorElement: <PaginaErrorRuta />,          // 6) captura també els fallats de càrrega
    handle: { molla: 'Inici' },
    children: [
      { index: true, element: <PaginaCataleg />, handle: { molla: 'Catàleg' } },

      {
        path: 'bicicletas/:bicicletaId',
        element: <Mandrosa><PaginaFitxaBicicleta /></Mandrosa>,
        handle: { molla: 'Fitxa de bicicleta' }
      },

      {
        path: 'estaciones',
        handle: { molla: 'Estacions' },
        children: [
          { index: true, element: <Mandrosa><PaginaEstacions /></Mandrosa> },
          {
            path: ':estacionId',
            element: <Mandrosa><PaginaDetallEstacio /></Mandrosa>,
            handle: {
              molla: (params) =>
                estacions.find((est) => est.id === params.estacionId)?.nom ?? 'Estació'
            },
            children: [
              { index: true, element: <Mandrosa><PestanyaFlota /></Mandrosa> },
              { path: 'incidencias', element: <Mandrosa><PestanyaIncidencies /></Mandrosa> }
            ]
          }
        ]
      },

      {
        path: 'reservas',
        element: <MarcReserves />,
        handle: { molla: 'Reserves' },
        children: [
          { index: true, element: <Mandrosa><PaginaReserves /></Mandrosa> },
          {
            path: 'nueva',
            element: <Mandrosa><PaginaNovaReserva /></Mandrosa>,
            handle: { molla: 'Nova reserva' }
          }
        ]
      },

      { path: 'acceso', element: <PaginaAcces />, handle: { molla: 'Accés' } },

      {
        element: <RutaProtegida />,
        children: [
          {
            element: <RequereixRol rolsPermesos={['operario']} />,
            children: [
              {
                path: 'taller',
                element: <Mandrosa><PaginaTaller /></Mandrosa>,
                handle: { molla: 'Taller' }
              }
            ]
          }
        ]
      },

      { path: '*', element: <PaginaNoTrobada /> }
    ]
  }
]);

Els sis punts marcats:

  1. PaginaCataleg es queda estàtica. És la ruta índex: dividir-la seria afegir una petició de xarxa extra just al camí crític. S'explica a l'apartat 7.
  2. PaginaAcces també. És petita, és la destinació de tota redirecció de RutaProtegida, i fer-la mandrosa introduiria un parpelleig en un moment sensible.
  3. PaginaErrorRuta no ha de ser mai mandrosa. Si el fallat que cal mostrar és precisament un fallat de xarxa, carregar el fragment de l'error també fallaria. Els components d'error s'empaqueten sempre al fragment principal.
  4. Un import() per pàgina = un fragment per pàgina. Sense configuració addicional.
  5. L'embolcall Mandrosa evita repetir <Suspense fallback={...}> en nou llocs i garanteix la mateixa experiència de càrrega a tots. Col·locar el Suspense dins de cada ruta, i no al voltant de l'<Outlet /> a Disseny, fa que només se substitueixi el contingut de la pàgina: la capçalera, les molles de pa i el peu no parpellegen.
  6. L'errorElement ja existent captura els errors llançats durant el render de les rutes filles, inclosos els rebutjos d'import(). Es detalla a l'apartat 11.

I el resultat, que és el punt sencer de la lliçó:

dist/assets/index-C8k2Nx9L.js         412.30 kB │ gzip: 128.44 kB   ← inicial
dist/assets/PaginaTaller-Bq7Wm3.js     38.12 kB │ gzip:  11.20 kB
dist/assets/PaginaDetallEstacio-D2.js 44.90 kB │ gzip:  13.05 kB
dist/assets/PestanyaFlota-Kl9x2.js     12.44 kB │ gzip:   3.90 kB
dist/assets/PestanyaIncidencies-Mn4.js 14.02 kB │ gzip:   4.31 kB
dist/assets/PaginaNovaReserva-Rp8.js   31.60 kB │ gzip:   9.44 kB
dist/assets/PaginaReserves-Ty3q.js     22.18 kB │ gzip:   6.72 kB
dist/assets/PaginaFitxaBicicleta-Zx.js 18.90 kB │ gzip:   5.88 kB
dist/assets/PaginaEstacions-Ab5.js     16.30 kB │ gzip:   5.02 kB
dist/assets/grafiques-Qw7e1.js        310.80 kB │ gzip:  94.60 kB
Abans Després
JavaScript inicial (gzip) 271 KB 128 KB
Reducció −53 %
Fragments 1 10
Cost de navegar al taller 0 (ja descarregat) Una petició de ~11 KB
Pressupost de 08-01 (< 200 KB) ❌ Incomplert ✅ Complert

  1. Per què el catàleg no es divideix

És temptador aplicar lazy a tot. Seria un error, i entendre per què distingeix qui optimitza de qui copia receptes.

La ruta d'entrada és la que l'usuari veu primer. Si la fas mandrosa, la seqüència s'allarga en lloc d'escurçar-se:

flowchart TD
    subgraph A["Ruta d'entrada ESTATICA (correcte)"]
        A1["Descarregar index.js"] --> A2["Executar"] --> A3["Pintar cataleg"]
    end
    subgraph B["Ruta d'entrada MANDROSA (error)"]
        B1["Descarregar index.js"] --> B2["Executar"] --> B3["Pintar el fallback"]
        B3 --> B4["Descarregar chunk del cataleg<br/>peticio EXTRA en cadena"]
        B4 --> B5["Executar"] --> B6["Pintar cataleg"]
    end

S'afegeix un viatge d'anada i tornada a la xarxa en sèrie, just al camí crític, i encara empitjora el LCP mostrant primer un esquelet. El codi estalviat és zero, perquè aquest codi fa falta igualment.

La regla general per decidir:

Divideix No divideixis
Rutes a què s'arriba navegant La ruta d'entrada
Pantalles d'un rol minoritari (PaginaTaller) Components del marc: Disseny, Capcalera, PeuDePagina
Modals i diàlegs que cal obrir Components d'error: PaginaErrorRuta, LimitError
Biblioteques pesades d'ús puntual (gràfiques, editors, mapes) Components petits: partir 3 KB no compensa la petició
Pestanyes que no són la pestanya índex Res que faci falta al primer pintat

I una regla de mida: per sota d'uns 20 KB sense comprimir, un fragment propi rarament compensa. Una petició HTTP té el seu propi cost de latència, i deu fragments minúsculs són pitjors que un de mitjà.

  1. Divisió per component

No tot el que és divisible és una ruta. Dins d'una mateixa pantalla hi ha parts que només existeixen si l'usuari fa alguna cosa.

Cas 1: DialegReserva. Viu a la fitxa de bicicleta però només es munta en prémer «Reservar». Arrossega el FormulariReserva, la seva validació i useBloquejarSortida.

// src/pagines/PaginaFitxaBicicleta.jsx
import { lazy, Suspense, useState } from 'react';
import { useParams } from 'react-router';
import Panell from '../components/Panell.jsx';
import IndicadorDeCarrega from '../components/IndicadorDeCarrega.jsx';
import { useBicicleta } from '../consultes/consultesBicicletes.js';

const DialegReserva = lazy(() => import('../components/DialegReserva.jsx'));

function PaginaFitxaBicicleta() {
  const { bicicletaId } = useParams();
  const { data: bicicleta, isPending } = useBicicleta(bicicletaId);
  const [dialegObert, setDialegObert] = useState(false);

  if (isPending) return <IndicadorDeCarrega missatge="Carregant bicicleta…" />;

  return (
    <Panell titol={bicicleta.model}>
      {/* … la fitxa … */}
      <button
        type="button"
        onClick={() => setDialegObert(true)}
        // Precàrrega en passar el ratolí: quan es premi, el fragment ja hi és
        onMouseEnter={() => import('../components/DialegReserva.jsx')}
        onFocus={() => import('../components/DialegReserva.jsx')}
        disabled={bicicleta.estat !== 'disponible'}
      >
        Reservar
      </button>

      {/* L'import() NO es dispara mentre dialegObert sigui false */}
      {dialegObert && (
        <Suspense fallback={<IndicadorDeCarrega missatge="Preparant la reserva…" />}>
          <DialegReserva
            bicicleta={bicicleta}
            alTancar={() => setDialegObert(false)}
          />
        </Suspense>
      )}
    </Panell>
  );
}

export default PaginaFitxaBicicleta;

El detall que fa que això funcioni: lazy no descarrega res fins que el component es renderitza de debò. Amb el renderitzat condicional {dialegObert && ...}, la petició es dispara en el moment d'obrir, no en muntar la pàgina.

Cas 2: la biblioteca de gràfiques. És el fragment de 95 KB comprimits, i només es fa servir a la pestanya de flota d'una estació, dins d'un desplegable d'ocupació setmanal.

// src/components/GraficaOcupacio.jsx
// Aquest mòdul importa la biblioteca de forma ESTÀTICA: és el seu propi fragment
import { GraficBarres, Eix, Barra, Reixeta } from 'una-biblioteca-de-graficas';

function GraficaOcupacio({ dades }) {
  return (
    <GraficBarres dades={dades} amplada={640} alcada={280}>
      <Reixeta />
      <Eix eix="x" clau="dia" />
      <Eix eix="y" />
      <Barra clau="lloguers" color="var(--color-marca)" />
    </GraficBarres>
  );
}

export default GraficaOcupacio;
// src/pagines/PestanyaFlota.jsx
import { lazy, Suspense, useState } from 'react';

// Com que és l'únic que importa la biblioteca, el fragment la inclou sencera
const GraficaOcupacio = lazy(() => import('../components/GraficaOcupacio.jsx'));

function PestanyaFlota() {
  const [veureGrafica, setVeureGrafica] = useState(false);

  return (
    <>
      <LlistaBicicletes bicicletes={bicicletes} />

      <button type="button" onClick={() => setVeureGrafica((v) => !v)}>
        {veureGrafica ? 'Amagar' : 'Veure'} ocupació setmanal
      </button>

      {veureGrafica && (
        <Suspense fallback={<EsqueletGrafica />}>
          <GraficaOcupacio dades={dadesSetmana} />
        </Suspense>
      )}
    </>
  );
}

La tècnica clau es diu aïllament per mòdul: es crea un component embolcall (GraficaOcupacio) que és el únic punt del projecte que importa la biblioteca pesada, i es carrega aquest embolcall de forma mandrosa. Així l'empaquetador pot posar la biblioteca sencera en un fragment a part. Si un altre mòdul la importés estàticament, tornaria al paquet principal i tot l'esforç es perdria.

Comprovació pràctica: després de npm run build, busca a dist/assets/ un fragment amb el pes de la biblioteca. Si no apareix i el paquet principal segueix gros, algú l'està important estàticament en algun altre lloc.

  1. Precàrrega: que l'espera no es noti

La divisió de codi canvia «esperar al principi» per «esperar en navegar». La precàrrega elimina la segona espera aprofitant el temps en què l'usuari s'està decidint: els centenars de mil·lisegons entre que el ratolí arriba a un enllaç i el dit prem.

// src/components/EnllacPrecarregat.jsx
import { Link } from 'react-router';

function EnllacPrecarregat({ to, carregar, children, ...resta }) {
  let precarregat = false;

  function precarregar() {
    if (precarregat) return;      // una sola vegada per instància
    precarregat = true;
    carregar();                   // dispara l'import() i el seu fragment
  }

  return (
    <Link
      to={to}
      onMouseEnter={precarregar}
      onFocus={precarregar}          // teclat: mateix comportament (03-06)
      onTouchStart={precarregar}     // mòbil: no hi ha hover, però sí un instant abans del tap
      {...resta}
    >
      {children}
    </Link>
  );
}

export default EnllacPrecarregat;
// src/components/Capcalera.jsx (fragment)
<nav>
  <EnllacPrecarregat to="/" carregar={() => {}}>Catàleg</EnllacPrecarregat>

  <EnllacPrecarregat
    to="/estaciones"
    carregar={() => import('../pagines/PaginaEstacions.jsx')}
  >
    Estacions
  </EnllacPrecarregat>

  <EnllacPrecarregat
    to="/reservas"
    carregar={() => import('../pagines/PaginaReserves.jsx')}
  >
    Les meves reserves
  </EnllacPrecarregat>

  {esOperari && (
    <EnllacPrecarregat
      to="/taller"
      carregar={() => import('../pagines/PaginaTaller.jsx')}
    >
      Taller
    </EnllacPrecarregat>
  )}
</nav>

Per què funciona tan bé: el navegador cacheja el mòdul, així que quan lazy demani el mateix import() en navegar, la promesa es resol de seguida i Suspense ni tan sols arriba a mostrar el fallback. L'usuari percep una navegació instantània encara que el codi s'hagi descarregat mig segon abans.

Estratègies de precàrrega, comparades:

Estratègia Quan dispara Risc Recomanació
En passar el ratolí / enfocar L'usuari apunta a l'enllaç Mínim: la intenció és alta ✅ Per defecte
En tocar (onTouchStart) Abans del click al mòbil Mínim: guanya ~80 ms ✅ Complementària
Després del pintat inicial (requestIdleCallback) Quan el fil és lliure Consumeix dades que potser no es facin servir Només per a 1–2 rutes molt probables
En aparèixer en pantalla (IntersectionObserver) L'enllaç entra a la vista Pot precarregar massa Per a llistes d'enllaços
Tot en arrencar Immediatament Anul·la la divisió de codi ❌ Mai

Un exemple de la tercera, per a la ruta que gairebé tothom visita després del catàleg:

// src/components/Disseny.jsx (fragment)
useEffect(() => {
  const id = requestIdleCallback?.(() => {
    import('../pagines/PaginaFitxaBicicleta.jsx');   // la ruta següent més probable
  });
  return () => cancelIdleCallback?.(id);
}, []);

  1. Dissenyar l'espera: esquelets en lloc de «Carregant…»

Un fallback mal dissenyat empitjora la percepció de rendiment encara que els mil·lisegons siguin idèntics. Dos motius, i tots dos es poden mesurar:

  • Salt de maquetació (CLS). Un <p>Carregant…</p> ocupa 20 píxels d'alt; la pàgina que arriba n'ocupa 900. En substituir-se, tot salta. Si l'usuari ja estava prement alguna cosa, prem on no volia.
  • Trencament del context. Un text centrat en una pantalla buida comunica «has sortit d'on eres». Un esquelet amb la forma del que ve comunica «això ja arriba».
// src/components/EsqueletPagina.jsx
import estils from './EsqueletPagina.module.css';

function EsqueletPagina({ files = 6 }) {
  return (
    <div className={estils.esquelet} role="status" aria-busy="true" aria-live="polite">
      <span className={estils.textAccessible}>Carregant contingut…</span>

      <div className={estils.titol} />
      <div className={estils.subtitol} />

      <div className={estils.reixeta}>
        {Array.from({ length: files }, (_, index) => (
          <div key={index} className={estils.targeta} />
        ))}
      </div>
    </div>
  );
}

export default EsqueletPagina;
/* src/components/EsqueletPagina.module.css */
.esquelet {
  min-height: 70vh;              /* reserva l'alçada: evita el salt (CLS) */
  padding: 1rem;
}

.titol,
.subtitol,
.targeta {
  background: linear-gradient(
    90deg,
    var(--color-vora) 25%,
    var(--color-superficie) 50%,
    var(--color-vora) 75%
  );
  background-size: 200% 100%;
  animation: brillantor 1.4s ease-in-out infinite;
  border-radius: 8px;
}

.titol    { height: 2rem;   width: 40%; margin-bottom: 0.75rem; }
.subtitol { height: 1rem;   width: 65%; margin-bottom: 1.5rem; }

.reixeta {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 1rem;
}

.targeta { height: 120px; }      /* la mateixa alçada que TargetaBicicleta */

@keyframes brillantor {
  from { background-position: 200% 0; }
  to   { background-position: -200% 0; }
}

/* Accessibilitat: respecta la preferència de moviment reduït (03-06) */
@media (prefers-reduced-motion: reduce) {
  .titol, .subtitol, .targeta { animation: none; }
}

.textAccessible {
  position: absolute;
  width: 1px; height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
}

Les decisions de disseny i el seu perquè:

Decisió Motiu
min-height: 70vh i alçades iguals a les reals El contingut real ocupa el mateix: zero salt de maquetació
role="status" + aria-busy + aria-live="polite" Un lector de pantalla anuncia la càrrega; sense això, l'espera és invisible (03-06)
Text accessible ocult Els rectangles no diuen res a qui no veu la pantalla
prefers-reduced-motion L'animació de brillantor pot molestar o marejar
Mateixa reixeta que el contingut real La transició es percep com un ompliment, no com un canvi de pantalla

I dues advertències que eviten l'efecte contrari al que es busca:

  • Un fallback que apareix i desapareix en 80 ms és pitjor que cap: es percep com un parpelleig. Si el fragment és petit i hi ha precàrrega, valora retardar l'aparició de l'esquelet (mostrar-lo només si l'espera supera els ~200 ms) o confiar en la precàrrega.
  • No posis un Suspense a l'arrel per a les rutes. Substituiria la capçalera i el peu, i cada navegació semblaria una recàrrega completa de la pàgina.

  1. Quan el fragment no arriba

Aquest és l'apartat que separa una divisió de codi de joguina d'una de producció. Un import() pot fallar, i falla més del que sembla:

Causa Freqüència Què veu l'usuari si no ho gestiones
Xarxa caiguda o túnel Alta al mòbil La pantalla es queda amb l'esquelet per sempre
Desplegament nou amb pestanya vella Molt alta en producció Error 404: el fragment amb aquell hash ja no existeix
Memòria cau intermèdia corrupta Baixa Error en analitzar el mòdul
Bloquejador d'anuncis agressiu Baixa Petició cancel·lada

El segon cas mereix explicació perquè sorprèn tothom: Vite posa un hash del contingut al nom de cada fragment (PaginaTaller-Bq7Wm3.js). Si desplegues una versió nova mentre algú té l'aplicació oberta, aquella pestanya continua executant el rutes.jsx vell, que demana un fragment amb un hash que ja no és al servidor. L'aplicació funciona perfectament… fins que l'usuari navega a una ruta que encara no havia visitat.

La solució combina Suspense (per a l'espera) amb LimitError (per al fallat), que és exactament la peça construïda a 04-05:

// src/components/LimitCarrega.jsx
import { Suspense } from 'react';
import LimitError from './LimitError.jsx';
import EsqueletPagina from './EsqueletPagina.jsx';
import estils from './LimitCarrega.module.css';

function FalladaDeCarrega({ error, alReintentar }) {
  // Un fragment que no arriba produeix un error d'importació dinàmica
  const esFalladaDeFragment =
    /Failed to fetch dynamically imported module|Importing a module script failed/i.test(
      error?.message ?? ''
    );

  return (
    <div className={estils.fallada} role="alert">
      <h2>No s'ha pogut carregar aquesta secció</h2>

      {esFalladaDeFragment ? (
        <p>
          Pot ser que hi hagi una versió nova de CicloUrbano disponible, o que la
          connexió s'hagi interromput. Torna a carregar la pàgina per continuar.
        </p>
      ) : (
        <p>S'ha produït un error inesperat en obrir aquesta secció.</p>
      )}

      <button type="button" onClick={() => window.location.reload()}>
        Recarregar la pàgina
      </button>
      <button type="button" onClick={alReintentar}>
        Reintentar sense recarregar
      </button>
    </div>
  );
}

function LimitCarrega({ children, fallback = <EsqueletPagina /> }) {
  return (
    // 1) El límit d'error va PER FORA: només així captura el rebuig de la promesa
    <LimitError alternativa={(error, reintentar) => (
      <FalladaDeCarrega error={error} alReintentar={reintentar} />
    )}>
      {/* 2) Suspense per dins: s'ocupa de l'espera, no del fallat */}
      <Suspense fallback={fallback}>{children}</Suspense>
    </LimitError>
  );
}

export default LimitCarrega;

L'ordre d'imbricació no és negociable:

flowchart TD
    A["LimitError<br/>captura el rebuig de l'import()"] --> B["Suspense<br/>mostra l'esquelet mentre carrega"]
    B --> C["Component mandros"]
    C -->|"promesa pendent"| D["Es veu l'esquelet"]
    C -->|"promesa resolta"| E["Es veu la pagina"]
    C -->|"promesa rebutjada"| F["L'error puja fins a LimitError<br/>es veu la fallada amb boto de recarrega"]

Si ho poséssim al revés —Suspense per fora i LimitError per dins—, el límit es destruiria en suspendre's l'arbre i no hi hauria ningú que capturés el fallat.

A rutes.jsx n'hi ha prou de substituir l'embolcall Mandrosa per LimitCarrega:

function Mandrosa({ children }) {
  return <LimitCarrega>{children}</LimitCarrega>;
}

Amb això, l'errorElement: <PaginaErrorRuta /> de la ruta arrel continua sent l'última xarxa de seguretat, i LimitCarrega ofereix un missatge específic i accionable —recarregar— al lloc on passa el fallat, sense tirar avall la navegació sencera. Recorda per què n'hi ha dos: errorElement captura els errors de les rutes i dels seus carregadors; LimitError captura els del render dels components. Un fallat d'import() passa durant el render, així que li correspon el segon.

Per què «recarregar» és l'acció correcta. En el cas més freqüent —desplegament nou— reintentar el mateix import() tornaria a demanar el fragment antic i tornaria a fallar. Recarregar la pàgina porta l'index.html nou, amb les referències correctes, i el problema desapareix. Per això el botó principal recarrega i el secundari reintenta.

  1. Altres palanques de mida

La divisió de codi és la palanca gran, però no l'única. Aquestes en són complement, i algunes són més barates d'aplicar:

Palanca Què fer Estalvi típic
Importar només el que es fa servir import { format } from 'date-fns' en lloc d'import * as fns Depèn: molt en biblioteques sense tree-shaking
Triar biblioteques més lleugeres date-fns o Intl en lloc de moment (que a més no es pot podar) 60–70 KB comprimits
Icones individuals import { BiBicycle } from 'react-icons/bi' en lloc del paquet sencer Desenes de KB
Carregar biblioteques sota demanda const { jsPDF } = await import('jspdf') dins del gestor d'«Exportar informe» Tot el pes de la biblioteca
import() de dades const { barris } = await import('../dades/barris.json') per a catàlegs grans La mida del JSON
Compressió al servidor Activar gzip i brotli al servidor o CDN Brotli sol millorar gzip un 15–20 %
Treure dependències mortes npx depcheck per localitzar el que ja ningú importa Variable, de vegades sorprenent
Objectiu de navegadors modern build.target: 'es2020' a Vite, sense transpilar de més 5–15 % del codi propi

Un exemple de càrrega sota demanda dins d'un gestor, que és un patró molt útil i sovint oblidat:

// src/pagines/PaginaTaller.jsx (fragment)
async function gestionarExportarInforme() {
  setExportant(true);
  try {
    // La biblioteca de PDF (~90 KB) només arriba si algú prem el botó
    const { generarInformePdf } = await import('../utilitats/informePdf.js');
    await generarInformePdf(incidencies);
    mostrarAvis('exit', 'Informe generat correctament.');
  } catch {
    mostrarAvis('error', 'No s\'ha pogut generar l\'informe.');
  } finally {
    setExportant(false);
  }
}

Aquí no cal lazy ni Suspense: no s'està carregant un component, sinó una funció. import() a seques dins d'una funció async és l'eina exacta, i el try/catch cobreix el fallat de xarxa.

I una advertència sobre la compressió que sovint es malinterpreta: gzip i brotli redueixen la transferència, no el treball de la CPU. Un paquet de 900 KB que es transfereix com a 270 KB continua havent-se d'analitzar i executar com a 900 KB. Als mòbils, això sol pesar més que la descàrrega.

  1. Suspense és molt més que això

<Suspense> ha aparegut aquí com «el que es mostra mentre arriba un fragment», i aquesta és només la seva aplicació més senzilla. El seu paper vertader a React és més general: és el mecanisme pel qual un component declara que encara no pot renderitzar-se i delega l'espera a un ancestre.

Sobre aquesta idea es construeixen coses que no caben en aquest mòdul:

  • Components que suspenen esperant dades, no codi: useSuspenseQuery de TanStack Query, o el hook use() de React 19.
  • Streaming SSR: el servidor envia l'HTML per parts i omple els forats de Suspense a mesura que les dades arriben.
  • React Server Components, on Suspense marca les fronteres entre el que es renderitza al servidor i el que arriba després.
  • Coordinació amb useTransition (08-03) perquè una navegació no mostri un fallback si la pantalla nova arriba prou de pressa.

Tot això és el contingut de 10-03. Aquí n'hi ha prou d'haver interioritzat el mecanisme: un component suspèn, el Suspense més proper per sobre mostra el seu fallback, i quan l'espera acaba l'arbre es completa.

Errors Comuns i Consells

Cridar lazy() dins d'un component. Cada render crearia un tipus de component diferent, React desmuntaria l'anterior i en muntaria un de nou, i la pantalla parpellejaria indefinidament. lazy() va sempre a l'àmbit del mòdul.

Oblidar el Suspense. Un component mandrós sense cap Suspense per sobre llança un error explícit en temps d'execució. No és opcional.

Posar un sol Suspense a l'arrel. Cada navegació substituiria tota l'aplicació, capçalera inclosa, i semblaria una recàrrega. Col·loca els límits de suspensió on vulguis que passi la substitució.

Fer mandrosa la ruta d'entrada. Afegeix una petició en sèrie al camí crític i empitjora el LCP sense estalviar ni un sol byte.

Fer mandrosos els components d'error. Si el que falla és la xarxa, el fragment de l'error tampoc arribarà. PaginaErrorRuta i LimitError van sempre al paquet principal.

Dividir massa. Vint fragments de 3 KB són pitjors que dos de 30 KB: cada petició té la seva latència. Divideix per unitats amb sentit —una ruta, un diàleg, una biblioteca pesada—, no per fitxer.

Suposar que el mòdul exporta per defecte. Si és una exportació amb nom, lazy fallarà amb un error confús. Adapta-ho amb .then((m) => ({ default: m.LoQueSigui })).

Consell: mesura sempre abans i després. npm run build abans de tocar res, anota la xifra comprimida, divideix, torna a construir i compara. Sense aquest parell de xifres no saps si has millorat res.

Consell: prova amb la xarxa alentida. A les eines del navegador, posa la xarxa en «3G lent» i navega. Allà es veu de debò si els esquelets funcionen, si la precàrrega arriba a temps i si el fallat de fragment està ben gestionat.

Consell: prova l'escenari del desplegament. Obre l'aplicació, torna a construir amb un canvi (els hashes canviaran), i navega a la pestanya vella cap a una ruta no visitada. Ha d'aparèixer el teu missatge de «versió nova disponible», no una pantalla en blanc.

Consell: revisa el mapa del paquet de tant en tant. Una dependència pesada es cola al paquet principal amb una sola importació estàtica despistada, i no hi ha cap avís que ho assenyali.

Exercicis

Exercici 1. Classifica cada element de CicloUrbano en estàtic, mandrós per ruta o mandrós per component, i justifica cada decisió en una frase.

# Element
a Capcalera
b PaginaTaller (només rol operario)
c Modal (usat per tres pantalles diferents)
d LimitError
e GraficaOcupacio (biblioteca de 95 KB comprimits)
f PaginaCataleg (ruta índex)
g EtiquetaEstat (3 KB)
h Un editor de text enriquit per a les notes d'incidència

Exercici 2. Aquest codi té quatre errors relacionats amb la càrrega mandrosa. Troba'ls, explica què provoca cadascun i escriu la versió corregida.

import { lazy, Suspense } from 'react';

function PaginaEstacions() {
  const [estacioOberta, setEstacioOberta] = useState(null);
  const PanellDetall = lazy(() => import('../components/PanellDetall.jsx'));

  return (
    <div>
      <ListaEstaciones alObrir={setEstacioOberta} />
      {estacioOberta && <PanellDetall estacio={estacioOberta} />}
    </div>
  );
}

// src/rutes.jsx
const PaginaErrorRuta = lazy(() => import('./pagines/PaginaErrorRuta.jsx'));
export const router = createBrowserRouter([
  { path: '/', element: <Disseny />, errorElement: <PaginaErrorRuta /> }
]);

Exercici 3. L'equip de CicloUrbano desplega tres vegades al dia. Els usuaris que deixen la pestanya oberta durant hores informen de pantalles en blanc en navegar al taller a la tarda. Explica la causa exacta, descriu la solució completa —incloent-hi l'ordre d'imbricació dels components— i digues per què el botó principal ha de recarregar la pàgina en lloc de reintentar la importació.

Solucions

Solució 1.

# Classificació Justificació
a Estàtic Forma part del marc: fa falta al primer pintat de totes les rutes
b Mandrós per ruta Només el veu un rol minoritari; és el cas ideal de divisió per ruta
c Estàtic El fan servir tres pantalles, així que acabaria duplicat o al fragment comú igualment; i és petit
d Estàtic Component d'error: si falla la xarxa, el seu fragment tampoc arribaria
e Mandrós per component, amb aïllament per mòdul 95 KB per a una gràfica dins d'un desplegable d'una pestanya. Ha de ser l'únic mòdul que importa la biblioteca
f Estàtic Ruta d'entrada: fer-la mandrosa afegeix una petició en sèrie sense estalviar res
g Estàtic 3 KB no compensen una petició HTTP; a més es fa servir a totes les targetes
h Mandrós per component Biblioteca pesada que només fa falta en començar a escriure una incidència. Precarrega-la en enfocar el camp

Solució 2. Els quatre errors:

  1. lazy() dins del component. A cada render es crea un component mandrós nou, React desmunta l'anterior i en munta un altre: parpelleig infinit i descàrregues repetides. Ha d'anar a l'àmbit del mòdul.
  2. Falta el Suspense. PanellDetall és mandrós i no té cap límit de suspensió per sobre: React llançarà un error en intentar renderitzar-lo.
  3. PaginaErrorRuta és mandrosa. És el component que s'ha de mostrar quan alguna cosa falla, inclòs un fallat de xarxa; si el seu propi fragment no arriba, no hi ha res a mostrar. Va sempre al paquet principal.
  4. Falta importar useState. Un error trivial, però real: el fitxer només importa lazy i Suspense.

Versió corregida:

import { lazy, Suspense, useState } from 'react';
import EsqueletPanell from '../components/EsqueletPanell.jsx';

// Fora del component: s'avalua una sola vegada
const PanellDetall = lazy(() => import('../components/PanellDetall.jsx'));

function PaginaEstacions() {
  const [estacioOberta, setEstacioOberta] = useState(null);

  return (
    <div>
      <ListaEstaciones alObrir={setEstacioOberta} />
      {estacioOberta && (
        <Suspense fallback={<EsqueletPanell />}>
          <PanellDetall estacio={estacioOberta} />
        </Suspense>
      )}
    </div>
  );
}
// src/rutes.jsx
import PaginaErrorRuta from './pagines/PaginaErrorRuta.jsx';   // estàtic, sempre

export const router = createBrowserRouter([
  { path: '/', element: <Disseny />, errorElement: <PaginaErrorRuta /> }
]);

Solució 3. Causa exacta: Vite inclou un hash del contingut al nom de cada fragment (PaginaTaller-Bq7Wm3.js). La pestanya oberta al matí continua executant el rutes.jsx d'aquella versió, que apunta als hashes antics. En desplegar a la tarda, el servidor serveix fragments amb hashes nous i esborra —o deixa de referenciar— els antics. Quan l'usuari navega al taller, ruta que encara no havia visitat, l'import() demana un fitxer que retorna 404, la promesa es rebutja, ningú captura l'error i la pantalla es queda en blanc (o amb l'esquelet per sempre, si el fallback continua muntat).

Solució completa: embolcallar cada ruta mandrosa en un LimitCarrega amb aquest ordre d'imbricació, que és l'únic que funciona:

LimitError  (per FORA: captura el rebuig de la promesa)
  └── Suspense  (per DINS: gestiona l'espera)
        └── Component mandros

Si s'invertís, el límit d'error es destruiria en suspendre's l'arbre i no quedaria ningú per capturar el fallat. L'alternativa del límit ha de distingir l'error d'importació dinàmica (Failed to fetch dynamically imported module) de la resta, donar un missatge comprensible —«potser hi ha una versió nova disponible»— i oferir una acció.

Per què recarregar i no reintentar: reintentar executa el mateix import(), que apunta al mateix hash inexistent, així que fallaria igualment tantes vegades com es premi. Recarregar la pàgina torna a demanar index.html, que porta les referències als fragments nous, i a partir d'aquest moment tot funciona. El reintent sense recarregar només té sentit en l'altre escenari —una interrupció momentània de xarxa amb el mateix desplegament vigent—, i per això s'ofereix com a acció secundària.

Com a millora addicional de l'equip: publicar un fitxer de versió i comprovar-lo periòdicament per avisar l'usuari («hi ha una versió nova, recarrega quan vulguis») abans que es trobi amb l'error, i conservar els fragments antics al servidor durant uns dies en lloc d'esborrar-los a cada desplegament.

Conclusió

Memoïtzar redueix el treball que fa React mentre l'usuari interactua; dividir el codi redueix el que el navegador ha de descarregar i executar abans que l'usuari pugui ni tan sols interactuar. Són problemes diferents i cap de les dues tècniques substitueix l'altra: el paquet únic de CicloUrbano pesava 271 KB comprimits —910 KB de JavaScript per analitzar i executar— amb la meitat dedicada a pantalles que la majoria de visites no obre.

El mètode ha estat el mateix de tot el mòdul: mesurar primer. npm run build dona la xifra i l'avís de Rollup sobre els 500 KB; rollup-plugin-visualizer diu què ocupa aquest espai, que és el que la xifra sola no explica. Sobre aquesta mesura, el mecanisme: l'import() dinàmic és la frontera automàtica entre fragments —una sintaxi de JavaScript, sense cap configuració—, i React.lazy és el pont entre aquesta promesa i JSX, amb dues exigències clares (exportació per defecte i crida a l'àmbit del mòdul) i un comportament que cal tenir present: la primera vegada suspèn i mostra el fallback, les següents és instantani.

La divisió per ruta és la que més rendeix, i a CicloUrbano ha deixat el JavaScript inicial en 128 KB comprimits, un 53 % menys, complint per fi el pressupost de 08-01. Igual d'important és el que no s'ha dividit: la ruta d'entrada, perquè fer-la mandrosa afegeix una petició en sèrie al camí crític sense estalviar cap byte; els components del marc; i sobretot els components d'error, perquè un fragment d'error que no arriba per culpa de la xarxa no serveix de res. A això s'hi suma la divisió per component —el DialegReserva que només es carrega en obrir-se i la GraficaOcupacio aïllada com a únic importador d'una biblioteca de 95 KB— i la càrrega de biblioteques dins d'un gestor amb import() a seques, sense lazy ni Suspense.

Després ve tot allò que converteix una divisió correcta en una experiència bona: la precàrrega en passar el ratolí, en enfocar amb el teclat i en tocar al mòbil, que aprofita els mil·lisegons de decisió de l'usuari perquè el fallback no arribi a veure's; els esquelets amb l'alçada i la forma del contingut real, amb role="status", text accessible ocult i respecte a prefers-reduced-motion, en lloc d'un «Carregant…» que provoca salts de maquetació i trenca el context; i la gestió del fallat de càrrega, amb LimitError per fora i Suspense per dins —l'ordre invers no captura res—, un missatge que distingeix el fallat de fragment de la resta i un botó que recarrega, perquè després d'un desplegament nou reintentar el mateix import() demanaria eternament un hash que ja no existeix. I les palanques complementàries: importar selectivament, triar biblioteques lleugeres, import() de dades, brotli al servidor i un objectiu de navegadors modern, amb el recordatori que la compressió redueix la transferència però no el treball de CPU.

Queda dit també que Suspense és molt més que un fallback de càrrega mandrosa —és el mecanisme general pel qual un component declara que encara no pot renderitzar-se— i que la seva història completa, juntament amb els Server Components, és a 10-03.

Amb això, CicloUrbano té totes les eines del mòdul: sap quin renderitzat evitar, com estabilitzar identitats, com abaratir càlculs i com repartir el seu codi en el temps. El que encara no té és la prova que res d'això serveixi. Cada xifra d'aquesta lliçó —271 KB, 128 KB, 53 %— ve d'una mesura, i cada memo i cada useMemo de les anteriors n'hauria de dur una altra. Falta l'eina que les produeix, que diu quin component s'ha repintat, quant ha trigat i per què, i que converteix una sospita en un diagnòstic. La propera lliçó és Mesurar el Rendiment amb React DevTools Profiler.

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