La lliçó anterior va acabar amb una pregunta oberta: saps què fa await, però no per què l'ordre de la sortida és el que és. Per què un console.log del nivell superior apareix després d'haver llançat tres operacions asíncrones; per què un .then sobre una promesa ja complerta s'executa més tard que el codi que hi ha a sota, però abans que un setTimeout(f, 0) registrat molt abans; per què un bucle pesat congela l'aplicació encara que el codi estigui ple d'await. Tot això ho decideix una maquinària que fins ara hem descrit amb la mà: el bucle d'esdeveniments. En aquesta lliçó la veuràs sencera —la pila de crides, les APIs de l'entorn, les dues cues i l'algorisme que les coordina—, predirs la sortida de fragments que semblen impossibles i els traçaràs pas a pas, i entendràs exactament què congela una interfície i què no. És la peça que converteix l'asincronia de «funciona per art de màgia» a «funciona així».

Contingut

  1. Les cinc peces
  2. Repàs: la pila de crides
  3. Les APIs de l'entorn: qui espera de debò
  4. Les dues cues: macrotasques i microtasques
  5. L'algorisme del bucle d'esdeveniments
  6. La regla d'or de les microtasques
  7. Exercici clàssic, resolt amb traça completa
  8. On encaixa await exactament
  9. Bloquejar el fil: què congela i què no
  10. Per què await no arregla un bucle pesat
  11. Trossejar la feina i cedir el fil
  12. requestAnimationFrame i temporitzadors imbricats
  13. Què implica tot això per al Mòdul 6
  14. Errors Habituals i Consells
  15. Exercicis
  16. Conclusió

  1. Les cinc peces

El sistema complet té cinc components. Només un d'ells és «JavaScript»; els altres els aporta l'entorn —el navegador o Node—.

Peça Què és Qui la proporciona
Pila de crides On s'executa el codi; una de sola El motor de JavaScript
APIs de l'entorn Temporitzadors, xarxa, esdeveniments, fitxers El navegador / Node
Cua de macrotasques Callbacks llestos de temporitzadors i esdeveniments L'entorn
Cua de microtasques Callbacks de promeses i queueMicrotask El motor
Bucle d'esdeveniments El coordinador que mou tasques a la pila L'entorn
flowchart TD
    subgraph Motor["Motor de JavaScript"]
        P["Pila de crides<br/>(una de sola, LIFO)"]
        MI["Cua de MICROtasques<br/>.then · await · queueMicrotask"]
    end
    subgraph Entorn["Entorn (navegador / Node)"]
        API["APIs: temporitzadors,<br/>xarxa, esdeveniments, fitxers"]
        MA["Cua de MACROtasques<br/>setTimeout · setInterval · esdeveniments"]
    end
    BE(["Bucle d'esdeveniments"])

    P -->|"registra l'operació"| API
    API -->|"en acabar, encua el callback"| MA
    P -->|"promesa saldada"| MI
    BE -->|"1 · pila buida?"| P
    BE -->|"2 · buida TOTES les microtasques"| MI
    BE -->|"3 · pren UNA macrotasca"| MA
    MI --> P
    MA --> P

La idea que cal retenir abans d'entrar en detall: la pila és l'únic lloc on s'executa codi, i el bucle d'esdeveniments és un porter que només deixa entrar alguna cosa nova quan la pila està completament buida.

  1. Repàs: la pila de crides

A 03-05 vas aprendre que cada crida a funció crea un context d'execució que s'apila, i que es desapila quan la funció retorna. És una estructura LIFO: l'últim que entra és el primer que surt.

'use strict';

function esforc(prioritat, hores) {
  return pesDePrioritat(prioritat) * hores;
}

function pesDePrioritat(prioritat) {
  return { alta: 3, mitjana: 2, baixa: 1 }[prioritat] ?? 0;
}

function informe() {
  const total = esforc('alta', 12);
  console.log(total);       // 36
}

informe();

La pila evoluciona així:

[informe]                                     ← es crida informe()
[informe, esforc]                             ← informe crida esforc
[informe, esforc, pesDePrioritat]             ← esforc crida pesDePrioritat
[informe, esforc]                             ← pesDePrioritat retorna 3
[informe]                                     ← esforc retorna 36
[informe, console.log]                        ← s'imprimeix
[]                                            ← pila buida

Aquell estat final —pila buida— és la condició que el bucle d'esdeveniments vigila. Mentre hi hagi alguna cosa a la pila, cap callback asíncron no pot començar. I com que la pila és única, «alguna cosa a la pila» significa que el fil està ocupat.

  1. Les APIs de l'entorn: qui espera de debò

Quan escrius setTimeout(f, 1000), aquella funció no és de JavaScript. El llenguatge, tal com el defineix la seva especificació, no sap mesurar el temps ni fer peticions de xarxa. setTimeout és una API de l'entorn: l'aporta el navegador (o Node).

El que passa pas a pas:

  1. El teu codi crida setTimeout(f, 1000). Aquesta crida entra a la pila.
  2. L'entorn en pren nota: guarda f i engega un temporitzador fora del fil de JavaScript.
  3. setTimeout retorna immediatament el seu identificador. Surt de la pila. El teu codi continua.
  4. Mil mil·lisegons després, el temporitzador venç. L'entorn fica f a la cua de macrotasques. Encara no s'executa res.
  5. Quan el bucle d'esdeveniments troba la pila buida, treu f de la cua i la fica a la pila. Ara sí que s'executa.

Això explica d'una vegada l'afirmació del 05-05 que «l'asincronia no fa res més ràpid»: qui espera no és el teu codi, sinó un altre component. El temporitzador el porta el sistema operatiu; la petició de xarxa la porta el subsistema HTTP del navegador, escrit en C++ i amb els seus propis fils. El teu programa només s'apunta al fet que l'avisin.

Les APIs de l'entorn més habituals:

API Què espera En quina cua encua el seu callback
setTimeout / setInterval Temps Macrotasques
Esdeveniments del DOM (clic, teclat) Interacció de l'usuari Macrotasques
fetch (07-02) Resposta de xarxa Microtasques (és una promesa)
Lectura de fitxers a Node Disc Macrotasques
requestAnimationFrame El repintat següent Cua pròpia (apartat 12)

  1. Les dues cues: macrotasques i microtasques

Aquí hi ha la subtilesa que explica gairebé tots els ordres sorprenents: no hi ha una cua, n'hi ha dues, i no tenen la mateixa prioritat.

Macrotasques (tasks) Microtasques (microtasks)
Què encua aquí setTimeout, setInterval, esdeveniments del DOM, E/S .then/.catch/.finally, represa després d'await, queueMicrotask
Quantes se'n processen per volta Una Totes, fins a buidar la cua
Prioritat Baixa Alta
Pot créixer la cua mentre es processa? Sí, però espera a la volta següent Sí, i es processa a la mateixa tanda
Es comprova entremig el repintat Sí, entre macrotasques No, fins que la cua estigui buida

La regla que es deriva de la segona fila és la més important de la lliçó:

Entre dues macrotasques, el motor buida la cua de microtasques per complet. Totes les promeses pendents de resoldre s'atenen abans que s'executi el setTimeout següent.

Això es demostra en quatre línies:

'use strict';

setTimeout(() => console.log('macrotasca'), 0);
Promise.resolve().then(() => console.log('microtasca'));
console.log('síncron');

// síncron
// microtasca      ← encara que es va registrar DESPRÉS del setTimeout
// macrotasca

Encara que el setTimeout es va escriure primer i el seu retard és zero, la microtasca s'atén abans. No és un detall d'implementació d'un navegador concret: és a l'especificació i es comporta igual a tots.

  1. L'algorisme del bucle d'esdeveniments

El bucle d'esdeveniments és, literalment, un bucle infinit que repeteix aquests passos:

mentre (el programa continuï viu) {

  1. Hi ha alguna cosa a la pila de crides?
        Sí → esperar. No es toca res.
        No → continuar.

  2. Buidar SENCERA la cua de microtasques:
        mentre (hi hagi microtasques) {
            treure la primera i executar-la fins al final
            (si genera microtasques noves, s'afegeixen a aquesta mateixa cua
             i també es processen ara)
        }

  3. [Al navegador] Si toca repintar, repintar ara.

  4. Prendre UNA macrotasca de la cua i executar-la fins al final.

  5. Tornar al pas 2.
}

Quatre conseqüències pràctiques es llegeixen directament d'aquell algorisme:

  • El codi síncron sempre guanya. El pas 1 espera que la pila estigui buida, i la pila no es buida fins que tot el script del nivell superior ha acabat.
  • Les microtasques van abans que les macrotasques, sempre, sense importar en quin ordre es van registrar (passos 2 i 4).
  • Una macrotasca s'executa sencera abans que s'atengui la següent. No hi ha interrupcions a mitja funció.
  • El repintat passa entre macrotasques, mai a mitges. És el que fa que un bucle llarg congeli la pantalla.

  1. La regla d'or de les microtasques

El pas 2 té una conseqüència perillosa: si una microtasca encua una altra microtasca, aquesta es processa a la mateixa tanda, no a la volta següent. Una cadena infinita de microtasques penja el programa per sempre, i el setTimeout no arriba a executar-se mai.

'use strict';

// ⚠ NO executis això: congela la pestanya
function bucleDeMicrotasques() {
  Promise.resolve().then(bucleDeMicrotasques);    // cadascuna encua la següent
}
setTimeout(() => console.log('mai no hi arribo'), 0);
bucleDeMicrotasques();

Compara-ho amb la versió equivalent feta amb macrotasques, que no penja res:

'use strict';

let cicles = 0;
function bucleDeMacrotasques() {
  cicles += 1;
  if (cicles < 1000) setTimeout(bucleDeMacrotasques, 0);
}
setTimeout(() => console.log('sí que hi arribo'), 0);
bucleDeMacrotasques();
// sí que hi arribo     ← perquè cada volta cedeix el torn

La diferència és exactament el pas 4 de l'algorisme: una macrotasca per volta, així que les altres tenen la seva oportunitat. Les microtasques no cedeixen.

Existeix a més una manera explícita d'encuar una microtasca, sense promesa pel mig:

queueMicrotask(() => console.log('microtasca explícita'));

Es fa servir poc en codi d'aplicació, però és útil per entendre el model: és l'equivalent exacte de Promise.resolve().then(...), sense crear una promesa.

Vols… Fes servir
Executar alguna cosa després del codi actual, abans de repintar queueMicrotask o Promise.resolve().then
Cedir el fil perquè la interfície respiri setTimeout(f, 0)
Executar just abans del repintat següent requestAnimationFrame (apartat 12)

  1. Exercici clàssic, resolt amb traça completa

Aquest fragment és l'examen estàndard del bucle d'esdeveniments. Llegeix-lo, aposta per un ordre i després segueix la traça.

'use strict';

console.log('1 · script');

setTimeout(() => console.log('2 · timeout A'), 0);

Promise.resolve()
  .then(() => console.log('3 · then A'))
  .then(() => console.log('4 · then B'));

async function tasca() {
  console.log("5 · dins de tasca, abans de l'await");
  await null;
  console.log("6 · dins de tasca, després de l'await");
}

tasca();

setTimeout(() => console.log('7 · timeout B'), 0);

queueMicrotask(() => console.log('8 · microtasca explícita'));

console.log('9 · fi del script');

Sortida:

1 · script
5 · dins de tasca, abans de l'await
9 · fi del script
3 · then A
6 · dins de tasca, després de l'await
8 · microtasca explícita
4 · then B
2 · timeout A
7 · timeout B

I ara la traça, instant a instant.

Fase síncrona (la pila té el script; el bucle d'esdeveniments no hi intervé):

Línia Què passa Cues en acabar
console.log('1') Imprimeix 1
setTimeout(…A…, 0) L'entorn engega el temporitzador
Promise.resolve().then(…) La promesa ja està complerta: encua then A micro: [then A]
.then(…B…) Es registra sobre una promesa pendent (la que retorna el primer .then). Encara no s'encua res micro: [then A]
tasca() Entra a la pila, imprimeix 5, arriba a l'await micro: [then A]
await null null s'embolcalla en una promesa complerta → encua la represa de tasca. La funció se suspèn i retorna al script micro: [then A, reprendre tasca]
setTimeout(…B…, 0) Un altre temporitzador engegat
queueMicrotask(…) Encua directament micro: [then A, reprendre tasca, micro explícita]
console.log('9') Imprimeix 9
Fi del script La pila es buida. Entra el bucle d'esdeveniments macro: [timeout A, timeout B]

Fixa't en la fila del segon .then: no s'ha encuat res. Un .then només encua el seu callback quan la promesa sobre la qual està registrat se salda, i la promesa que retorna el primer .then continua pendent fins que aquest s'executi. Aquell detall és el que fa que 4 · then B acabi darrere de 8.

Pas 2 de l'algorisme: buidar les microtasques.

Es treu Imprimeix Efecte sobre la cua
then A 3 La seva promesa es compleix → encua then B. Cua: [reprendre tasca, micro explícita, then B]
reprendre tasca 6 La funció tasca continua després de l'await i acaba
micro explícita 8
then B 4 Cua buida → se surt del pas 2

Aquí es veu la regla d'or en acció: then B es va afegir durant el buidatge i tot i així es va processar a la mateixa tanda, abans de tocar cap macrotasca.

Pas 4: una macrotasca. S'executa timeout A → imprimeix 2. Tornada al pas 2: no hi ha microtasques. Macrotasca següent: timeout B → imprimeix 7.

flowchart TD
    S["FASE SÍNCRONA<br/>1 · 5 · 9"] --> M1["MICROTASQUES (totes)<br/>3 → 6 → 8 → 4"]
    M1 --> R["repintat (si toca)"]
    R --> T1["MACROTASCA 1<br/>2 · timeout A"]
    T1 --> M2["microtasques (cap)"]
    M2 --> T2["MACROTASCA 2<br/>7 · timeout B"]

Si has encertat l'ordre complet a la primera, has entès el model. Si no, la part que sol fallar és la de 4 · then B: recorda que els .then encadenats no s'encuen tots alhora, sinó un darrere l'altre a mesura que es van complint les seves promeses.

  1. On encaixa await exactament

Ja tens la peça que faltava del 05-06. await fa tres coses:

  1. Avalua l'expressió que té al davant i, si no és una promesa, l'embolcalla en una de complerta.
  2. Suspèn la funció i retorna el control a qui l'ha cridada. La pila es desmunta fins allà.
  3. Registra la represa com a microtasca, que s'executarà quan la promesa se saldi i li arribi el torn.

D'aquí en surten dues conclusions importants.

El cos d'una funció async és síncron fins al primer await. Això sorprèn molt:

'use strict';

async function carregar() {
  console.log('A · això és síncron');
  const tasques = await llegirBacklog();
  console.log('C · això és una microtasca');
  return tasques;
}

carregar();
console.log('B · això va després de A');

// A · això és síncron
// B · això va després de A
// C · això és una microtasca   (quan la promesa se saldi)

Cridar una funció async no difereix el seu començament: s'executa immediatament fins que troba un await. És útil saber-ho: si vols que una funció async validi els seus arguments i falli ràpid, posa les validacions abans del primer await i llançaran en el mateix instant de la crida… bé, gairebé: com que la funció retorna una promesa, el throw es converteix en un rebuig. Però la feina ja es fa.

Cada await costa com a mínim una volta de microtasques. Encara que la promesa ja estigui complerta:

async function tres() {
  console.log('1');
  await null;          // microtasca
  console.log('2');
  await null;          // una altra microtasca
  console.log('3');
}

tres();
Promise.resolve().then(() => console.log('X'));
Promise.resolve().then(() => console.log('Y'));

// 1
// X          ← s'intercalen: cada await cedeix el torn
// 2
// Y
// 3

Aquell intercalat és la prova visual que await no és una pausa màgica: és un return seguit d'una represa encuada com a microtasca. I explica per què posar cinquanta await innecessaris en una funció té un cost real, encara que petit.

  1. Bloquejar el fil: què congela i què no

Ara es pot explicar amb precisió el problema que obria el 05-05.

'use strict';

console.log('Comença el càlcul');

let suma = 0;
for (let i = 0; i < 2_000_000_000; i++) {
  suma += i;                       // uns quants segons ocupant la pila
}

console.log('Acaba el càlcul', suma);

Mentre aquell bucle corre, la pila no es buida. I si la pila no es buida:

  • el pas 1 de l'algorisme no passa mai;
  • cap microtasca no es processa;
  • cap macrotasca no es processa;
  • no hi ha repintat (pas 3), així que la pantalla es queda congelada exactament com estava;
  • els clics de l'usuari s'encuen com a macrotasques i s'executaran tots de cop en acabar, amb el desconcert corresponent.

Aquell és l'estat que el navegador acaba reportant com «la pàgina no respon».

Convé distingir amb claredat què bloqueja i què no:

Operació Bloqueja el fil? Per què
Bucle de mil milions d'iteracions Ocupa la pila tota l'estona
JSON.parse d'un fitxer de 50 MB És codi síncron, per ràpid que sigui
Ordenar un array d'un milió d'elements sort és síncron
await llegirBacklog() No Suspèn la funció i allibera la pila
setTimeout(f, 5000) No El temporitzador el porta l'entorn
alert('hola') , i de manera brutal És síncron i bloquejant per disseny

La regla es resumeix en una frase: el que bloqueja no és esperar, és calcular. Una espera ben feta allibera el fil; un càlcul llarg el segresta, sigui quina sigui la sintaxi que l'envolti.

  1. Per què await no arregla un bucle pesat

Un error molt estès és pensar que embolcallar el càlcul en una funció async el fa no bloquejant.

'use strict';

// ✗ Continua congelant exactament igual
async function calcularEsforcTotal(tasques) {
  let total = 0;
  for (let i = 0; i < 2_000_000_000; i++) {
    total += i;
  }
  return total;
}

console.log('abans');
calcularEsforcTotal([]);          // la interfície es congela igual
console.log('després');           // surt uns quants segons més tard

El motiu el saps des de l'apartat 8: el cos d'una funció async és síncron fins al primer await, i aquí no n'hi ha cap. async no crea un fil ni trasllada res a una altra banda; només canvia el que la funció retorna.

I ficar-hi un await que no espera res tampoc no serveix:

async function tampocArregla(tasques) {
  await null;                     // cedeix el fil UNA vegada, al principi
  for (let i = 0; i < 2_000_000_000; i++) { /* … */ }   // i després el segresta igual
}

Després d'aquella microtasca, el bucle torna a ocupar la pila durant segons. L'única manera real de no bloquejar amb un càlcul pesat és una d'aquestes dues:

  1. Trossejar-lo i cedir el fil entre trossos (apartat 11).
  2. Treure'l del fil principal amb un Web Worker, que sí que executa JavaScript en un fil a part amb el seu propi bucle d'esdeveniments. És la solució correcta per a feina intensiva de debò, i s'estudia a 09-02.

  1. Trossejar la feina i cedir el fil

Trossejar consisteix a processar un lot, tornar el control al bucle d'esdeveniments i programar el lot següent com a macrotasca. Aplicat a un backlog gegantí de Taller Nómada:

'use strict';

/**
 * Processa un array per lots sense bloquejar el fil.
 * @param {Array}    elements
 * @param {Function} accio      què fer amb cada element
 * @param {number}   midaLot    quants per volta
 * @returns {Promise<void>}
 */
function processarPerLots(elements, accio, midaLot = 500) {
  return new Promise((resolve) => {
    let index = 0;

    function seguentLot() {
      const fi = Math.min(index + midaLot, elements.length);
      for (; index < fi; index++) {
        accio(elements[index], index);
      }
      if (index < elements.length) {
        setTimeout(seguentLot, 0);         // ← cedeix el fil: entra una volta del bucle
      } else {
        resolve();
      }
    }

    seguentLot();
  });
}

// Ús amb un backlog simulat de 200 000 tasques
const enorme = Array.from({ length: 200_000 }, (_, i) => ({ id: i + 1, horesEstimades: (i % 40) + 1 }));

let hores = 0;
await processarPerLots(enorme, (t) => { hores += t.horesEstimades; }, 1000);
console.log(`Total: ${hores} h`);

Entre lot i lot, el bucle d'esdeveniments completa una volta: processa microtasques, repinta i atén els clics de l'usuari. L'aplicació continua viva. El preu és que el procés complet triga una mica més —cada setTimeout imbricat té el seu mínim d'uns 4 ms—, i aquí hi ha el compromís: la mida del lot decideix l'equilibri entre fluïdesa i velocitat total.

Una alternativa moderna, quan l'objectiu és que la interfície respiri:

// Només en navegadors que ho admetin
await scheduler.yield();      // "cedeix el torn i reprèn tan aviat com puguis"

I un advertiment important: no facis servir await d'una promesa ja complerta per cedir el fil. Com que és una microtasca, es processa a la mateixa volta, sense repintat ni atenció a esdeveniments. Per cedir de debò cal una macrotasca (setTimeout) o una API específica.

Tècnica Cedeix el fil de debò?
await null / await Promise.resolve() No: microtasca
await new Promise((r) => setTimeout(r, 0)) : macrotasca
setTimeout(seguent, 0)
Web Worker Sí, i a més fa servir un altre nucli (09-02)

  1. requestAnimationFrame i temporitzadors imbricats

Dos apunts breus per completar el mapa, que reprendràs al Mòdul 6 i al 9.

requestAnimationFrame(callback) programa una funció perquè s'executi just abans del repintat següent, sincronitzada amb la freqüència de la pantalla (unes 60 vegades per segon en un monitor normal). No és ni macrotasca ni microtasca: té el seu propi moment a l'algorisme, el pas 3.

function animar(marcaDeTemps) {
  // …actualitzar posicions…
  requestAnimationFrame(animar);       // es reprograma per al fotograma següent
}
requestAnimationFrame(animar);

La diferència amb setTimeout(f, 16) és que rAF està sincronitzat amb el repintat: no s'executa si la pestanya està amagada —cosa que estalvia bateria— i mai no produeix dues actualitzacions entre dos fotogrames. Per a qualsevol animació, rAF és l'opció correcta; setTimeout produeix salts.

Temporitzadors imbricats. Ja ho vam apuntar a 05-05: per especificació, a partir del cinquè setTimeout imbricat els navegadors eleven el mínim a uns 4 ms. Així que un setTimeout(f, 0) que es reprograma a si mateix no fa mil voltes per segon, sinó unes dues-centes cinquanta. És una limitació deliberada per evitar que un bucle de temporitzadors cremi el processador, i és la raó per la qual trossejar amb setTimeout té un cost que cal mesurar.

  1. Què implica tot això per al Mòdul 6

A la lliçó següent tanques el Mòdul 5, i al 6 comences a construir la interfície de Nómada Tasques. Tot el que acabes d'aprendre es torna molt concret allà. Anota aquestes cinc conseqüències:

  1. Els gestors d'esdeveniments són macrotasques. Cada clic en un botó «Marcar com a feta» encua una macrotasca. Si el teu gestor triga 300 ms, l'usuari percep la interfície com enganxosa, perquè no es repinta res mentre dura.

  2. Repintar només passa entre macrotasques. Si en un mateix gestor canvies deu vegades el text d'un element, l'usuari veurà només l'últim valor: no hi ha repintat a mitja funció. És una bona notícia —evita parpellejos— i explica per què els canvis s'agrupen de manera natural.

  3. Un càlcul pesat en un gestor congela l'aplicació sencera, amb la llista de tasques a mig pintar i els botons sense respondre. La lògica costosa es trosseja, es treu a un worker o es fa fora del gestor.

  4. L'ordre entre await i esdeveniments importa. Si un gestor de clic fa await desarTasca(), l'usuari pot prémer el botó una altra vegada durant l'espera i encuar un segon gestor. Desactivar el botó mentre l'operació està en marxa no és un adorn: és correcció.

  5. <script type="module">defer implícit (05-04), així que el teu codi s'executa amb l'HTML ja construït, i aquella execució és una macrotasca més dins del cicle de vida de la pàgina.

Amb això, el comportament de la interfície deixa de ser misteriós: és l'algorisme de l'apartat 5, aplicat a esdeveniments d'usuari.

Errors Habituals i Consells

  • Creure que setTimeout(f, 0) executa ja. Només encua. El retard és un mínim, i a més cal esperar que la pila es buidi.
  • Creure que async fa que alguna cosa no bloquegi. async només canvia el que retorna la funció. El que bloqueja és calcular, no la sintaxi.
  • Fer servir await Promise.resolve() per «deixar respirar» la interfície. És una microtasca: es processa a la mateixa volta, sense repintat. Cal una macrotasca.
  • Cadenes infinites de microtasques. Una microtasca que n'encua una altra sense condició de parada penja el programa sense remei i sense missatge d'error.
  • Confiar en el temps exacte d'un setInterval. Si el fil està ocupat quan venç, el callback es retarda; i si es retarda molt, els navegadors fusionen repeticions. Per mesurar temps real, fes servir marques amb Date.now() o performance.now().
  • Suposar un ordre fix entre callbacks de tipus diferents. Entre dos setTimeout amb el mateix retard, l'ordre de registre mana; entre una macrotasca i una microtasca, guanya sempre la micro. Però no suposis res més enllà d'això.
  • Depurar el bucle d'esdeveniments amb console.log esperant veure la pila. El panell Performance de les DevTools dibuixa la pila, les tasques i els repintats en una línia temporal: és l'eina adequada, i s'estudia a 08-01 i 09-01.
  • Consell: quan un ordre asíncron et sorprengui, escriu-lo com la traça de l'apartat 7: una taula amb la cua de microtasques i la de macrotasques després de cada línia. En cinc minuts es resol qualsevol dubte.

Exercicis

Exercici 1 — Predir i traçar. Indica la sortida exacta d'aquest fragment i construeix la taula de la traça (estat de les dues cues després de cada línia síncrona).

console.log('inici');

setTimeout(() => {
  console.log('T1');
  Promise.resolve().then(() => console.log('T1-micro'));
}, 0);

setTimeout(() => console.log('T2'), 0);

Promise.resolve().then(() => {
  console.log('P1');
  setTimeout(() => console.log('P1-timeout'), 0);
});

(async () => {
  console.log('async abans');
  await Promise.resolve();
  console.log('async després');
})();

console.log('fi');

Exercici 2 — Mesurar el bloqueig. Escriu dues versions d'una funció que sumi l'esforç ponderat d'un array de 300 000 tasques simulades: una de síncrona i una altra trossejada amb processarPerLots. Mesura amb Date.now() quant triga cadascuna i, sobretot, comprova amb un setInterval que imprimeixi un punt cada 50 ms quina de les dues permet que aquell interval continuï funcionant durant el càlcul.

Exercici 3 — Diagnòstic. Aquest codi pretén mostrar un avís mentre carrega i amagar-lo en acabar, però l'avís «no es veu mai». Explica per què fent servir l'algorisme del bucle d'esdeveniments i proposa la correcció.

function carregarAmbAvis() {
  mostrarAvis('Carregant…');               // canvia l'estat que es pintarà
  const resultat = calculPesatSincron();   // 3 segons
  ocultarAvis();
  return resultat;
}

Solucions

Exercici 1

Sortida:

inici
async abans
fi
P1
async després
T1
T1-micro
T2
P1-timeout

Traça de la fase síncrona:

Línia executada Imprimeix Cua de microtasques Cua de macrotasques
console.log('inici') inici
setTimeout(T1, 0) [T1]
setTimeout(T2, 0) [T1, T2]
Promise.resolve().then(P1) [P1] [T1, T2]
IIFE async fins a l'await async abans [P1, reprendre-async] [T1, T2]
console.log('fi') fi [P1, reprendre-async] [T1, T2]

Pila buida → pas 2, buidar microtasques: P1 imprimeix P1 i encua P1-timeout a macrotasques (queda [T1, T2, P1-timeout]); reprendre-async imprimeix async després. Cua de microtasques buida.

Pas 4, una macrotasca: T1 imprimeix T1 i encua T1-micro a microtasques. Tornada al pas 2: es buida la cua → T1-micro. Macrotasca següent: T2. Després: P1-timeout.

El punt fi de l'exercici és que T1-micro surt immediatament després de T1, abans que T2, encara que T2 feia estona que esperava a la seva cua: en acabar cada macrotasca es buiden totes les microtasques pendents.

Exercici 2

'use strict';

const PESOS = { alta: 3, mitjana: 2, baixa: 1 };
const PRIORITATS = ['alta', 'mitjana', 'baixa'];

const enorme = Array.from({ length: 300_000 }, (_, i) => ({
  id: i + 1,
  prioritat: PRIORITATS[i % 3],
  horesEstimades: (i % 40) + 1
}));

// ── Versió síncrona ───────────────────────────────────────────────
function esforcSincron(tasques) {
  let total = 0;
  for (const t of tasques) total += PESOS[t.prioritat] * t.horesEstimades;
  return total;
}

// ── Versió trossejada ─────────────────────────────────────────────
function esforcTrossejat(tasques, midaLot = 5000) {
  return new Promise((resolve) => {
    let index = 0;
    let total = 0;

    function lot() {
      const fi = Math.min(index + midaLot, tasques.length);
      for (; index < fi; index++) {
        total += PESOS[tasques[index].prioritat] * tasques[index].horesEstimades;
      }
      if (index < tasques.length) setTimeout(lot, 0);
      else resolve(total);
    }
    lot();
  });
}

// ── El "batec" que revela si el fil està lliure ───────────────────
let batecs = 0;
const pols = setInterval(() => { batecs += 1; }, 50);

let t0 = Date.now();
const a = esforcSincron(enorme);
const tempsSincron = Date.now() - t0;
const batecsDurantSincron = batecs;

batecs = 0;
t0 = Date.now();
const b = await esforcTrossejat(enorme);
const tempsTrossejat = Date.now() - t0;
const batecsDurantTrossejat = batecs;

clearInterval(pols);

console.log(`Síncron: ${a} en ${tempsSincron} ms · batecs: ${batecsDurantSincron}`);
console.log(`Trossejat: ${b} en ${tempsTrossejat} ms · batecs: ${batecsDurantTrossejat}`);
// Síncron: 82000000 en 12 ms · batecs: 0
// Trossejat: 82000000 en 320 ms · batecs: 6

Els números concrets varien segons la màquina, però el patró és sempre el mateix i és el que cal llegir:

  • La versió síncrona és més ràpida en total —no paga el cost dels setTimeout— però registra zero batecs: durant tot el càlcul, l'interval no es va executar ni una vegada. El fil estava segrestat, i en una aplicació real això significa pantalla congelada.
  • La versió trossejada triga bastant més però permet diversos batecs: entre lot i lot, el bucle d'esdeveniments va completar voltes senceres, atenent temporitzadors, repintant i responent a clics.

Aquesta és la decisió d'enginyeria en estat pur: se sacrifica temps total a canvi que l'aplicació continuï viva. Si el càlcul cap folgadament en uns pocs mil·lisegons, no trossegis; si durarà més d'uns 50 ms, trosseja o porta-ho a un Web Worker.

Exercici 3

L'avís no es veu perquè el repintat passa al pas 3 de l'algorisme, entre macrotasques, i aquí la pila no es buida mai. La seqüència real és:

  1. mostrarAvis('Carregant…') canvia l'estat intern de la pàgina… però no dibuixa res: només marca que cal repintar.
  2. calculPesatSincron() ocupa la pila tres segons. El bucle d'esdeveniments no arriba mai al pas 3, així que no hi ha repintat.
  3. ocultarAvis() desfà el canvi, encara sense que la pila s'hagi buidat.
  4. La funció retorna, la pila es buida i per fi es repinta… mostrant l'estat final, en el qual l'avís ja està amagat.

L'usuari veu tres segons de congelació i cap avís. La correcció consisteix a deixar que passi un repintat entre mostrar l'avís i començar el càlcul, cedint el fil amb una macrotasca:

const cedirElFil = () => new Promise((resolve) => setTimeout(resolve, 0));

async function carregarAmbAvis() {
  mostrarAvis('Carregant…');
  await cedirElFil();                      // ✓ el bucle completa una volta i REPINTA
  try {
    return await esforcTrossejat(dades);   // ✓ i a més no congela mentre calcula
  } finally {
    ocultarAvis();                         // s'executa passi el que passi (02-05)
  }
}

Dos detalls que cal subratllar. L'await cedirElFil() ha de ser una macrotasca: si escrivissis await Promise.resolve() seria una microtasca, es processaria a la mateixa volta i no hi hauria repintat, amb la qual cosa el problema continuaria exactament igual. I trossejar el càlcul amb esforcTrossejat resol la segona meitat del problema: sense això, l'avís es veuria, però la interfície continuaria congelada durant els tres segons.

Conclusió

Ja no queda res de màgia a l'asincronia de JavaScript. El sistema té cinc peces: una pila de crides única on s'executa tot el codi; les APIs de l'entorn, que són qui espera de debò —el temporitzador el porta el sistema operatiu, la xarxa la porta el navegador—; una cua de macrotasques per als callbacks de temporitzadors i esdeveniments; una cua de microtasques per a les promeses, els await i queueMicrotask; i el bucle d'esdeveniments, un porter que només deixa entrar alguna cosa nova quan la pila està completament buida.

El seu algorisme cap en quatre passos, i d'ells es dedueix tota la resta: esperar que la pila es buidi, buidar sencera la cua de microtasques, repintar si toca i prendre una sola macrotasca abans de tornar a començar. D'aquí surten les tres regles operatives que has de tenir gravades: el codi síncron sempre guanya; les microtasques s'atenen totes abans que la macrotasca següent, sense importar en quin ordre es van registrar; i una cadena infinita de microtasques penja el programa mentre que una de macrotasques no, perquè aquestes cedeixen el torn a cada volta. Has traçat l'exercici clàssic línia a línia i has vist per què then B acaba darrere de la microtasca explícita —els .then encadenats no s'encuen de cop, sinó a mesura que es compleixen les seves promeses—, i saps exactament què fa await: avalua, suspèn la funció tornant el control, i encua la represa com a microtasca, de manera que el cos d'una funció async és síncron fins al primer await i cada await costa com a mínim una volta.

I tens el diagnòstic correcte del bloqueig: el que congela no és esperar, és calcular. Un await allibera la pila; un bucle de dos mil milions d'iteracions la segresta, i amb ella se'n van les microtasques, les macrotasques, els clics de l'usuari i —el més visible— el repintat. Per això embolcallar el càlcul en una funció async no arregla res, i per això await Promise.resolve() tampoc no cedeix el fil de debò: és una microtasca. Les sortides reals són dues: trossejar la feina cedint amb una macrotasca entre lots, acceptant conscientment trigar més a canvi que l'aplicació continuï viva, o treure-la del fil principal amb un Web Worker (09-02). Saps a més que requestAnimationFrame té el seu propi moment, just abans del repintat, i que els temporitzadors imbricats tenen un mínim d'uns 4 ms.

Amb això tanques la part asíncrona del mòdul, i tens anotades les cinc conseqüències que t'esperen al Mòdul 6: els gestors d'esdeveniments són macrotasques, el repintat només passa entre ells, un càlcul pesat dins d'un gestor congela la interfície sencera, i un botó que dispara una operació asíncrona cal desactivar-lo mentre dura. Queda una última peça del llenguatge per descobrir, i és la que explica com funciona per dins una cosa que fas servir des del Mòdul 2 sense preguntar-t'ho: què fa exactament for...of quan recorre un array, un Map o un Set, com es pot fer que un Tauler sigui recorrible amb aquella mateixa sintaxi, i com es generen seqüències que es calculen només quan es demanen —incloses les infinites i les asíncrones—. És el tema d'Iteradors i Generadors, l'última lliçó del mòdul.

Curs de JavaScript: De Principiant a Avançat

Mòdul 1: Introducció a JavaScript

Mòdul 2: Estructures de Control

Mòdul 3: Funcions

Mòdul 4: Objectes i Arrays

Mòdul 5: Objectes i Funcions Avançades

Mòdul 6: El Model d'Objectes del Document (DOM)

Mòdul 7: APIs del Navegador i Temes Avançats

Mòdul 8: Proves i Depuració

Mòdul 9: Rendiment i Optimització

Mòdul 10: Frameworks i Llibreries de JavaScript

Mòdul 11: Projecte Final

© Copyright 2026. Tots els drets reservats