Les sis lliçons anteriors tracten de JavaScript parlant amb el navegador: desar, demanar, sincronitzar, posar a la memòria cau, formatar. Aquesta tracta d'una cosa diferent: codi que no és JavaScript executant-se a la mateixa pàgina, dins del mateix motor, a velocitat gairebé nativa. WebAssembly —Wasm per als amics— és un format binari portable que permet portar al web programes escrits en C, C++, Rust o Go. Gràcies a ell existeixen editors de vídeo, motors de joc, eines de CAD i bases de dades completes funcionant dins d'una pestanya. En aquesta lliçó entendràs què és i, sobretot, què no és; veuràs el seu model d'execució i per què només entén números; carregaràs i instanciaràs un mòdul des de JavaScript; coneixeràs les eines reals de cada llenguatge; i ho aplicaràs tot a un cas concret del Taller Nómada —una simulació de 200.000 combinacions d'assignació de tasques— per acabar fent l'única cosa que tanca la discussió: mesurar si compensa. Avanço la conclusió, perquè és la lliçó més important: gairebé mai compensa, i Nómada Tasques no ho necessita.

Contingut

  1. Què és WebAssembly
  2. Què NO és WebAssembly
  3. Per què existeix: rendiment predictible i codi reutilitzable
  4. El model d'execució: mòdul, instància, memòria i taula
  5. Per què només entén números
  6. El format de text .wat davant del binari .wasm
  7. Carregar i instanciar des de JavaScript
  8. Creuar la frontera: exportar, importar i compartir memòria
  9. Les eines reals: Rust, C/C++ i AssemblyScript
  10. Casos d'ús reals
  11. JavaScript davant de Wasm: comparativa honesta
  12. WASI i el model de components
  13. El cas del Taller Nómada: la planificació pesada
  14. Mesurar si compensa
  15. Errors Habituals i Consells
  16. Exercicis
  17. Conclusió

  1. Què és WebAssembly

WebAssembly és un format d'instruccions binari per a una màquina virtual de pila. Quatre adjectius el defineixen:

  • Binari: no és text que s'analitza com JavaScript, sinó bytes amb una estructura fixa que el navegador pot validar i compilar molt de pressa.
  • Portable: el mateix .wasm funciona igual en qualsevol navegador, sistema operatiu i arquitectura de processador.
  • Segur: s'executa a la mateixa caixa de sorra que JavaScript, sense accés al sistema de fitxers, a la xarxa ni a la memòria del procés llevat del que li donis explícitament.
  • Ràpid: es compila a codi màquina i assoleix velocitats properes a les natives.

La idea clau, i la que més es malinterpreta:

WebAssembly no reemplaça JavaScript. S'executa al mateix motor, al costat de JavaScript, i tots dos es criden mútuament. És un company, no un substitut.

En un navegador modern, el teu app.js i un mòdul .wasm conviuen a la mateixa pestanya, comparteixen el mateix fil (tret que facis servir workers) i s'invoquen com a funcions normals.

És a més un estàndard del W3C des del 2019, amb suport a tots els navegadors moderns des del 2017. No és experimental.

  1. Què NO és WebAssembly

Aquesta secció importa més que l'anterior, perquè gairebé tots els malentesos vénen d'aquí.

Mite Realitat
«És el substitut de JavaScript» Conviuen. Wasm ni tan sols pot tocar el DOM sense passar per JavaScript
«Tot és més ràpid en Wasm» Només el càlcul intensiu. Manipular el DOM o fer peticions és igual o més lent pel cost de creuar la frontera
«Serveix per amagar el meu codi» El binari es descarrega igualment i es descompila amb eines públiques. No és ofuscació ni protecció
«Puc accedir al sistema de fitxers» No al navegador. És a la mateixa caixa de sorra, i només fa el que JavaScript li permeti
«És un llenguatge de programació» És un format de destinació de compilació. Ningú l'escriu a mà llevat que sigui per aprendre
«Serveix per a qualsevol aplicació web» Per a la immensa majoria —inclosa Nómada Tasques— no aporta res

I la limitació pràctica més rellevant: Wasm no té accés directe al DOM. Si un mòdul vol canviar el text d'una targeta, ha de cridar una funció JavaScript que tu li hagis passat com a importació. Això significa que una aplicació la feina principal de la qual sigui pintar interfície —com la nostra— no hi guanya res, perquè el coll d'ampolla no és al càlcul.

  1. Per què existeix: rendiment predictible i codi reutilitzable

Wasm va néixer per resoldre dos problemes concrets.

Rendiment predictible. Els motors de JavaScript són extraordinàriament ràpids gràcies a la compilació just-in-time: analitzen el codi en execució, dedueixen els tipus, generen codi màquina optimitzat. Però aquella optimització és especulativa, i es pot desfer:

function sumar(a, b) { return a + b; }

sumar(1, 2);            // el motor optimitza assumint enters
sumar(1.5, 2.5);        // continua bé: números
sumar('a', 'b');        // ← DESOPTIMITZACIÓ: havia assumit números, ara hi ha cadenes

Aquell fenomen s'anomena deoptimization, i fa que el rendiment de JavaScript sigui excel·lent però variable. WebAssembly, en canvi, té tipus estàtics, sense recol·lector d'escombraries al seu nucli i sense especulació: el seu rendiment és predictible, cosa que en un motor de joc a 60 fps importa més que la velocitat mitjana.

Reutilitzar codi existent. Hi ha dècades de biblioteques escrites en C i C++ —còdecs de vídeo, criptografia, motors físics, processament d'imatge, SQLite— que representen milions d'hores de feina. Reescriure-les en JavaScript seria absurd. Compilar-les a Wasm les porta al web tal qual.

flowchart LR
    subgraph Fonts
        R["Rust"]
        C["C / C++"]
        A["AssemblyScript"]
        G["Go, Zig, Swift…"]
    end
    R --> W["mòdul .wasm"]
    C --> W
    A --> W
    G --> W
    W --> M["Motor del navegador<br/>(el mateix que executa JS)"]
    JS["JavaScript"] --> M
    M --> CPU["Codi màquina"]

  1. El model d'execució: mòdul, instància, memòria i taula

Quatre conceptes, i convé distingir-los bé perquè els noms s'assemblen.

Concepte Què és Analogia en JavaScript
Mòdul (WebAssembly.Module) El codi compilat, sense estat. Es pot reutilitzar i posar a la memòria cau Una classe
Instància (WebAssembly.Instance) Un mòdul amb la seva memòria i el seu estat, llest per executar Un objecte creat amb new
Memòria (WebAssembly.Memory) Un bloc de bytes contigu i redimensionable Un ArrayBuffer gegant
Taula (WebAssembly.Table) Un array de referències a funcions Un array de funcions, per a crides indirectes

La memòria lineal és la peça central i la més aliena a qui ve de JavaScript. És literalment un ArrayBuffer: una tira de bytes numerats des de 0, sense estructura. No hi ha objectes, ni cadenes, ni arrays: només bytes que el mòdul interpreta segons el tipus que espera a cada posició.

flowchart TB
    subgraph Pagina["Pestanya del navegador"]
        JS["JavaScript<br/>objectes, cadenes, GC"]
        subgraph Inst["Instància Wasm"]
            F["Funcions exportades"]
            MEM["Memòria lineal<br/>(ArrayBuffer de bytes)"]
            TAB["Taula de funcions"]
        end
    end
    JS -->|"crida exports.f(3, 4)"| F
    F -->|"llegeix i escriu"| MEM
    JS <-->|"new Uint8Array(memory.buffer)"| MEM
    F -->|"crida imports.avisar()"| JS

L'important d'aquell diagrama: JavaScript pot llegir i escriure la memòria de Wasm directament, mitjançant vistes tipades (Uint8Array, Float64Array…) sobre el mateix ArrayBuffer. No hi ha còpia. Aquell accés compartit és l'única manera de passar dades grans d'un costat a l'altre amb eficiència.

Wasm té només quatre tipus numèrics a la seva versió base: i32, i64, f32 i f64 (enters i decimals de 32 i 64 bits). Res més. Ni booleans, ni caràcters, ni punters de debò: un punter és senzillament un i32 que representa un desplaçament dins de la memòria lineal.

  1. Per què només entén números

Del que hem dit se'n segueix la conseqüència més pràctica de tota la lliçó: passar una cadena a Wasm no és passar una cadena. Cal serialitzar-la.

Per enviar 'Pressupost de la fusteria' a un mòdul cal: codificar-la a bytes UTF-8, reservar espai a la memòria lineal, escriure aquells bytes allà, i passar a la funció el desplaçament i la longitud, dos números.

/** Escriu una cadena a la memòria de Wasm i retorna on és i quant ocupa. */
function escriureCadena(instancia, text) {
  const bytes = new TextEncoder().encode(text);               // cadena → bytes UTF-8

  // El mòdul ha d'exportar un assignador; aquí en suposem un de simple
  const punter = instancia.exports.reservar(bytes.length);

  const memoria = new Uint8Array(instancia.exports.memory.buffer);
  memoria.set(bytes, punter);                                 // còpia byte a byte

  return { punter, longitud: bytes.length };
}

/** I el camí de tornada */
function llegirCadena(instancia, punter, longitud) {
  const memoria = new Uint8Array(instancia.exports.memory.buffer, punter, longitud);
  return new TextDecoder('utf-8').decode(memoria);
}

Aquell anar i venir és el cost de creuar la frontera, i explica per què Wasm no sempre guanya:

Què hi passes Cost
Un número (i32, f64) Gairebé zero: va directe
Un array de números Baix, si escrius a la memòria compartida sense copiar
Una cadena Mitjà: codificar, reservar, copiar, i el mateix a la tornada
Un objecte o un array d'objectes Alt: cal aplanar-lo a bytes en un format acordat

D'aquí la regla que governa el disseny de qualsevol integració amb Wasm:

Creua la frontera poques vegades amb molta feina, mai moltes vegades amb poca. Una crida que processa 200.000 elements és excel·lent; 200.000 crides que en processen un cadascuna són un desastre, i seran més lentes que fer-ho tot en JavaScript.

Un matís per no quedar-te amb una idea desactualitzada: la proposta de referències a tipus de l'amfitrió i la integració amb el recol·lector d'escombraries (WasmGC, ja disponible en diversos navegadors) redueixen aquesta fricció per a llenguatges amb GC com Java, Kotlin o Dart. Però el model mental de la memòria lineal continua sent el que necessites per a C, C++ i Rust.

  1. El format de text .wat davant del binari .wasm

Wasm té dues representacions equivalents: el binari .wasm que es descarrega, i un format de text .wat llegible per humans, pensat per depurar i aprendre.

;; sumar.wat — el "hola món" de WebAssembly
(module
  ;; Declara una funció anomenada "sumar" que rep dos i32 i retorna un i32
  (func $sumar (param $a i32) (param $b i32) (result i32)
    local.get $a        ;; posa $a a la pila
    local.get $b        ;; posa $b a la pila
    i32.add)            ;; treu els dos, suma, deixa el resultat a la pila

  ;; La fa visible des de JavaScript amb el nom "sumar"
  (export "sumar" (func $sumar))
)

Es llegeix de dalt a baix com una màquina de pila: cada instrucció treu operands de la pila i hi deixa resultats. local.get $a empeny el primer paràmetre; local.get $b empeny el segon; i32.add treu tots dos i empeny la suma, que queda com a valor de retorn.

Un exemple una mica més real, amb memòria:

(module
  ;; 1 pàgina de memòria = 64 KiB. S'exporta perquè JavaScript la pugui llegir
  (memory (export "memory") 1)

  ;; Suma tots els f64 d'un array que comença a $ptr i té $n elements
  (func $sumarArray (param $ptr i32) (param $n i32) (result f64)
    (local $i i32)
    (local $total f64)

    (loop $bucle
      (br_if 1 (i32.ge_u (local.get $i) (local.get $n)))    ;; si i >= n, sortir

      ;; total += memoria[ptr + i*8]   (un f64 ocupa 8 bytes)
      (local.set $total
        (f64.add (local.get $total)
                 (f64.load (i32.add (local.get $ptr)
                                    (i32.mul (local.get $i) (i32.const 8))))))

      (local.set $i (i32.add (local.get $i) (i32.const 1)))
      (br $bucle)
    )
    (local.get $total)
  )
  (export "sumarArray" (func $sumarArray))
)

Fixa't en el nivell al qual es treballa: els índexs es multipliquen per 8 a mà perquè un f64 ocupa vuit bytes, i el bucle es construeix amb salts explícits. Ningú escriu Wasm a mà per a producció; es compila des d'un llenguatge d'alt nivell. Veure el .wat serveix per a tres coses: entendre el model, depurar el que ha generat el teu compilador, i comprovar la mida del resultat.

Per convertir entre formats es fa servir WABT (WebAssembly Binary Toolkit):

wat2wasm sumar.wat -o sumar.wasm     # text → binari
wasm2wat sumar.wasm -o sumar.wat     # binari → text (es pot descompilar!)
wasm-objdump -x sumar.wasm           # inspeccionar seccions

Aquell wasm2wat és la prova que Wasm no protegeix el teu codi: qualsevol pot recuperar una versió llegible del mòdul que serveixes.

  1. Carregar i instanciar des de JavaScript

Hi ha quatre maneres de carregar un mòdul, i una és clarament la millor:

Funció Entrada Retorna Quan
WebAssembly.instantiateStreaming(resposta, imports) Una Response de fetch { module, instance } La recomanada
WebAssembly.instantiate(bytes, imports) ArrayBuffer { module, instance } Si ja tens els bytes
WebAssembly.compileStreaming(resposta) Una Response Module Compilar ara, instanciar després
new WebAssembly.Instance(module, imports) Module Instance Síncron: només per a mòduls petits ja compilats
// js/wasm/carregar.js
export async function carregarModul(url, importacions = {}) {
  // instantiateStreaming compila MENTRE es descarrega: no espera l'últim byte
  const { instance, module } = await WebAssembly.instantiateStreaming(
    fetch(url),
    importacions
  );
  return { exports: instance.exports, module };
}
const { exports } = await carregarModul('/wasm/sumar.wasm');
console.log(exports.sumar(3, 4));      // 7   ← es crida com una funció normal

instantiateStreaming és la manera correcta perquè compila en paral·lel a la descàrrega, en lloc d'esperar a tenir-ho tot. Però té un requisit estricte que provoca l'error més freqüent de tots:

El servidor ha d'enviar Content-Type: application/wasm. Si no, el navegador llança TypeError: Incorrect response MIME type.

Molts servidors estàtics ja ho fan; alguns no. La reserva, per si de cas:

export async function carregarModulRobust(url, importacions = {}) {
  try {
    return await WebAssembly.instantiateStreaming(fetch(url), importacions);
  } catch (error) {
    console.warn('[wasm] El streaming ha fallat, es carrega per ArrayBuffer:', error.message);
    const resposta = await fetch(url);
    if (!resposta.ok) throw new Error(`HTTP ${resposta.status}`);   // l'ok de 07-02
    const bytes = await resposta.arrayBuffer();
    return WebAssembly.instantiate(bytes, importacions);
  }
}

I comprovar disponibilitat, amb el mateix patró de millora progressiva de 07-06:

export const HI_HA_WASM = typeof WebAssembly === 'object'
  && typeof WebAssembly.instantiateStreaming === 'function';

  1. Creuar la frontera: exportar, importar i compartir memòria

Exportar és el que el mòdul ofereix a JavaScript: funcions, memòria, taules i variables globals. Tot apareix a instance.exports.

console.log(Object.keys(instancia.exports));
// ['memory', 'sumar', 'sumarArray', 'reservar']

Importar és el que JavaScript ofereix al mòdul. Es passa com a segon argument, agrupat per espais de noms:

const importacions = {
  env: {
    // El mòdul pot cridar funcions nostres
    avisar: (codi) => console.log('[wasm] avís', codi),
    ara: () => Date.now(),

    // O fer servir una memòria que creem nosaltres
    memory: new WebAssembly.Memory({ initial: 16, maximum: 256 })   // pàgines de 64 KiB
  }
};

const { instance } = await WebAssembly.instantiateStreaming(fetch('/wasm/pla.wasm'), importacions);

Aquí hi ha la resposta a «com toca Wasm el DOM?»: no el toca. Li passes una funció JavaScript que sí que pot, i el mòdul la crida. Tot el que Wasm pot fer amb el món exterior passa per les importacions que tu li donis, i això és exactament el que el fa segur.

Compartir memòria és la manera eficient de moure dades en volum:

/** Copia un array de JavaScript a la memòria de Wasm i crida la funció. */
function calcularSobreArray(instancia, valors) {
  const { memory, reservar, sumarArray } = instancia.exports;

  const bytes = valors.length * 8;                     // Float64Array: 8 bytes per número
  const punter = reservar(bytes);

  // Una VISTA sobre la memòria de Wasm: no hi ha còpia del buffer, només interpretació
  const vista = new Float64Array(memory.buffer, punter, valors.length);
  vista.set(valors);                                   // aquí sí que es copien les dades

  return sumarArray(punter, valors.length);
}

I un parany que costa hores de descobrir:

Si la memòria creix (per una crida a memory.grow() o per una reserva interna del mòdul), l'ArrayBuffer anterior queda desvinculat i totes les teves vistes deixen de funcionar. Crea la vista just abans de fer-la servir, no la desis mai en una variable de vida llarga.

// ✗ Perillós: si la memòria creix, aquesta vista queda inservible
const MEMORIA = new Uint8Array(instancia.exports.memory.buffer);

// ✓ Crear la vista en el moment de fer-la servir
const vista = () => new Uint8Array(instancia.exports.memory.buffer);

  1. Les eines reals: Rust, C/C++ i AssemblyScript

A la pràctica ningú escriu .wat. Es compila des d'un llenguatge d'alt nivell, i cadascun té la seva cadena d'eines.

Eina Llenguatge Fort en Corba Mida típica
wasm-pack + wasm-bindgen Rust La millor integració amb JavaScript; genera la cola automàticament Alta (aprendre Rust) Petita (desenes de KB)
Emscripten C / C++ Portar codi existent, incloses biblioteques enormes Mitjana Mitjana a gran
AssemblyScript Subconjunt de TypeScript Començar sense aprendre un altre llenguatge Baixa Molt petita
TinyGo Go Reutilitzar codi Go Mitjana Mitjana
wasm-bindgen (sol) Rust Control fi dels enllaços Alta

Rust amb wasm-pack és l'opció més completa avui. wasm-bindgen genera automàticament el codi JavaScript que tradueix cadenes, structs i objectes, estalviant-te tota la gestió manual de la memòria:

// src/lib.rs
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn millor_assignacio(hores: &[f64], capacitat: f64) -> f64 {
    // Aquí sí que pots fer servir cadenes i structs: wasm-bindgen genera la cola!
    hores.iter().filter(|h| **h <= capacitat).sum()
}
cargo install wasm-pack
wasm-pack build --target web        # genera pkg/ amb el .wasm i el .js d'enllaç
import init, { millor_assignacio } from './pkg/planificador.js';

await init();                        // carrega i instancia el .wasm
console.log(millor_assignacio(new Float64Array([12, 8, 5]), 10));   // 13

AssemblyScript és la millor porta d'entrada per a qui ve de JavaScript, perquè s'escriu amb sintaxi de TypeScript:

// planificador.ts — AssemblyScript: sembla TypeScript, compila a Wasm
export function esforcPonderat(pesos: Int32Array, hores: Float64Array): f64 {
  let total: f64 = 0;
  for (let i = 0; i < pesos.length; i++) {
    total += f64(pesos[i]) * hores[i];
  }
  return total;
}
npm install --save-dev assemblyscript
npx asinit .
npx asc planificador.ts --target release --outFile build/planificador.wasm

Compte amb una confusió habitual: AssemblyScript no és TypeScript. Comparteix sintaxi, però té tipus propis (i32, f64, u8), no té tipus dinàmics ni closures complets, i el seu recol·lector d'escombraries és diferent. És un llenguatge a part amb roba familiar.

Emscripten compila C i C++ i fins i tot emula parts del sistema operatiu (fitxers, SDL, OpenGL), cosa que permet portar programes complets:

emcc planificador.c -O3 -s EXPORTED_FUNCTIONS='["_simular"]' \
     -s MODULARIZE=1 -o planificador.js

És l'eina que ha portat al web coses com ffmpeg o SQLite.

  1. Casos d'ús reals

Wasm no és teòric: hi ha programari important funcionant amb ell.

Àmbit Exemples Per què Wasm
Edició d'imatge i vídeo Figma, Photoshop web, ffmpeg.wasm Milions de píxels per fotograma; el càlcul domina
Bases de dades SQLite compilat a Wasm (sql.js, wa-sqlite) Motor complet, en C, funcionant al navegador
Criptografia libsodium.js, implementacions de hash Codi auditat que no convé reescriure; temps constant
Jocs i 3D Unity, Unreal, motors propis Rendiment predictible a 60 fps, motors en C++
CAD i enginyeria AutoCAD web, simuladors Dècades de codi C++ irreemplaçable
Científic Pyodide (Python sencer al navegador), R, NumPy Ecosistemes complets sense servidor
Compiladors i llenguatges Playgrounds de Rust, Go, Zig Executar el compilador real al client
Fora del navegador Plugins d'Envoy, Fastly, Shopify Caixa de sorra portable i ràpida per a codi de tercers

Aquell últim cas és interessant: Wasm va començar al navegador, però la seva combinació de portabilitat i aïllament l'està convertint en un format de plugins per a servidors i plataformes.

Fixa't en el patró que comparteixen tots: càlcul intensiu sobre moltes dades, o codi existent que no es pot reescriure. Cap no és una aplicació de formularis i llistes.

  1. JavaScript davant de Wasm: comparativa honesta

Aspecte JavaScript WebAssembly
Velocitat en càlcul intensiu Bona, amb JIT Millor i més predictible (1,2× a 3× típicament)
Velocitat manipulant el DOM Nativa Pitjor: cada operació creua la frontera
Temps fins a la primera execució Immediat Descàrrega + compilació del mòdul
Mida de descàrrega Només el teu codi El mòdul inclou el seu runtime: Rust ~30-100 KB, Emscripten sovint més
Cost de cridar una funció Zero Baix per crida, alt si hi passes cadenes o objectes
Depuració Excel·lent: DevTools completes Millorant (mapes de fonts), encara incòmoda
Corba d'aprenentatge Ja el saps Un altre llenguatge + la seva cadena d'eines
Ecosistema Enorme Petit i especialitzat
Accés a APIs del navegador Total Només el que li importis des de JavaScript
Manteniment Un llenguatge Dos llenguatges, dues compilacions, dos equips

Les tres coses que més se subestimen en avaluar Wasm:

  • La mida. Un mòdul Rust senzill són desenes de KB comprimits; un d'Emscripten amb biblioteca estàndard pot ser centenars. Si el teu càlcul triga 40 ms en JavaScript i el mòdul pesa 300 KB, descarregar-lo costa més del que estalvia.
  • El cost de la frontera. Si el mòdul es crida moltes vegades amb poca feina, la traducció de dades es menja el guany i acabes més lent.
  • El cost humà. Afegir Rust a un projecte de JavaScript significa una altra cadena de compilació, altres dependències, un altre coneixement a l'equip i un altre punt de fallada al desplegament. És una decisió d'arquitectura, no un ajust tècnic.

Quan compensa de debò, resumit en tres condicions que s'han de donar alhora:

  1. La feina és càlcul pur i prolongat (desenes de mil·lisegons o més).
  2. Es creua la frontera poques vegades amb moltes dades.
  3. O bé ja existeix el codi en C/C++/Rust i reescriure'l seria absurd.

Si en falta alguna, la resposta correcta és JavaScript. I si el problema és que la interfície es congela, hi ha una alternativa molt més barata que Wasm: moure el càlcul a un Web Worker, que continua sent JavaScript però en un altre fil.

  1. WASI i el model de components

Dues peces del futur de Wasm que convé conèixer de nom.

WASI (WebAssembly System Interface) és un conjunt estàndard d'APIs que dona als mòduls Wasm accés controlat a capacitats del sistema —fitxers, rellotge, xarxa, variables d'entorn— fora del navegador. Amb WASI, un .wasm és un binari portable que corre igual a Linux, macOS i Windows, amb permisos explícits per capacitat. És la base de plataformes com Wasmtime, Wasmer i dels plugins a la vora de la xarxa.

El model de components aborda el problema del qual hem parlat tota l'estona: la frontera. Defineix tipus d'alt nivell —cadenes, registres, llistes, variants— mitjançant un llenguatge d'interfície (WIT), de manera que un component escrit en Rust pugui cridar-ne un altre escrit en Python sense que ningú escrigui codi de traducció a mà.

Cap de les dues no canvia avui la teva manera de treballar al navegador, i per això queden aquí esmentades. Però expliquen cap a on va la tecnologia: de «un format per accelerar càlculs al web» a «un format universal, segur i portable per executar codi de tercers a qualsevol lloc».

  1. El cas del Taller Nómada: la planificació pesada

Construirem un exemple honest: un càlcul real de Nómada Tasques que que és pesat, i mesurarem si Wasm compensa.

El problema. La Marta vol saber com repartir les tasques obertes entre l'Iván, la Lucía i ella perquè ningú superi les 40 hores setmanals (regla R7) minimitzant l'esforç ponderat desequilibrat. Amb 5 tasques obertes i 3 persones hi ha 3⁵ = 243 combinacions: trivial. Però en planificar el trimestre sencer, amb 11 tasques i 3 persones, són 3¹¹ ≈ 177.000; i explorant també l'ordre dins de cada persona, se superen fàcilment les 200.000 combinacions.

En JavaScript:

// js/planificacio/simulador.js — versió JavaScript, la referència
import { PESOS } from '../util/format.js';

const EQUIP = ['Marta', 'Iván', 'Lucía'];
const MAX_HORES = 40;                                  // R7

/**
 * Prova totes les assignacions possibles i retorna la de menor desequilibri.
 * Complexitat: 3^n. Amb n = 11, unes 177.000 combinacions.
 */
export function millorRepartiment(tasques) {
  const n = tasques.length;
  const combinacions = 3 ** n;

  let millorCost = Infinity;
  let millorAssignacio = null;

  const hores = new Float64Array(3);
  const esforc = new Float64Array(3);

  for (let c = 0; c < combinacions; c += 1) {
    hores.fill(0);
    esforc.fill(0);

    // Cada combinació es codifica en base 3: el dígit i diu a qui va la tasca i
    let resta = c;
    let valida = true;

    for (let i = 0; i < n; i += 1) {
      const persona = resta % 3;
      resta = Math.floor(resta / 3);

      hores[persona] += tasques[i].horesEstimades;
      if (hores[persona] > MAX_HORES) { valida = false; break; }     // poda per R7
      esforc[persona] += PESOS[tasques[i].prioritat] * tasques[i].horesEstimades;
    }
    if (!valida) continue;

    // Cost = diferència entre qui més i qui menys esforç carrega
    const cost = Math.max(...esforc) - Math.min(...esforc);
    if (cost < millorCost) {
      millorCost = cost;
      millorAssignacio = c;
    }
  }

  return { cost: millorCost, codi: millorAssignacio, combinacions };
}

Aquest codi és un bon candidat a Wasm perquè compleix les tres condicions: és càlcul pur, es crida una vegada amb totes les dades, i treballa exclusivament amb números.

La versió en AssemblyScript, deliberadament semblant:

// wasm/planificador.ts (AssemblyScript)
const MAX_HORES: f64 = 40;

export function millorRepartiment(horesPtr: usize, pesosPtr: usize, n: i32): f64 {
  let combinacions: i32 = 1;
  for (let i = 0; i < n; i++) combinacions *= 3;

  let millorCost: f64 = f64.MAX_VALUE;

  for (let c = 0; c < combinacions; c++) {
    let h0: f64 = 0, h1: f64 = 0, h2: f64 = 0;
    let e0: f64 = 0, e1: f64 = 0, e2: f64 = 0;
    let resta = c;
    let valida = true;

    for (let i = 0; i < n; i++) {
      const persona = resta % 3;
      resta = resta / 3;

      // Lectura directa de la memòria lineal: 8 bytes per f64, 4 per i32
      const hores = load<f64>(horesPtr + i * 8);
      const pes   = f64(load<i32>(pesosPtr + i * 4));

      if (persona == 0)      { h0 += hores; if (h0 > MAX_HORES) { valida = false; break; } e0 += pes * hores; }
      else if (persona == 1) { h1 += hores; if (h1 > MAX_HORES) { valida = false; break; } e1 += pes * hores; }
      else                   { h2 += hores; if (h2 > MAX_HORES) { valida = false; break; } e2 += pes * hores; }
    }
    if (!valida) continue;

    const max = Math.max(e0, Math.max(e1, e2));
    const min = Math.min(e0, Math.min(e1, e2));
    if (max - min < millorCost) millorCost = max - min;
  }
  return millorCost;
}

I el pont des de JavaScript:

// js/planificacio/pont-wasm.js
let instancia = null;

export async function iniciarPlanificador(url = '/wasm/planificador.wasm') {
  if (instancia !== null) return instancia;                       // instanciar UNA vegada
  const { instance } = await WebAssembly.instantiateStreaming(fetch(url), {
    env: { abort: () => { throw new Error('[wasm] abort'); } }
  });
  instancia = instance;
  return instancia;
}

export function millorRepartimentWasm(tasques) {
  const { memory, __new, millorRepartiment } = instancia.exports;
  const n = tasques.length;

  // Reservar i escriure els dos arrays a la memòria lineal
  const horesPtr = __new(n * 8, 0);
  const pesosPtr = __new(n * 4, 0);

  // Vistes creades ARA: la memòria ha pogut créixer en reservar
  new Float64Array(memory.buffer, horesPtr, n).set(tasques.map((t) => t.horesEstimades));
  new Int32Array(memory.buffer, pesosPtr, n).set(tasques.map((t) => PESOS[t.prioritat]));

  return millorRepartiment(horesPtr, pesosPtr, n);                // UNA sola travessia
}

Observa el que s'ha aconseguit: es creua la frontera una vegada per escriure les dades i una vegada per cridar. Tota la feina de 177.000 iteracions passa dins de Wasm. És el disseny correcte.

  1. Mesurar si compensa

I ara l'única cosa que tanca la discussió. Qualsevol afirmació sobre rendiment sense mesura és una opinió.

// js/planificacio/comparar.js
import { millorRepartiment } from './simulador.js';
import { iniciarPlanificador, millorRepartimentWasm } from './pont-wasm.js';

/** Executa una funció diverses vegades i retorna la mediana, més robusta que la mitjana. */
function mesurar(nom, funcio, repeticions = 7) {
  const temps = [];
  funcio();                                           // escalfament: deixa que el JIT optimitzi

  for (let i = 0; i < repeticions; i += 1) {
    const inici = performance.now();
    funcio();
    temps.push(performance.now() - inici);
  }
  temps.sort((a, b) => a - b);
  const mediana = temps[Math.floor(temps.length / 2)];
  console.log(`${nom}: ${mediana.toFixed(1)} ms (mín ${temps[0].toFixed(1)})`);
  return mediana;
}

export async function comparar(tasques) {
  const iniciCarrega = performance.now();
  await iniciarPlanificador();
  const msCarrega = performance.now() - iniciCarrega;

  const msJs   = mesurar('JavaScript', () => millorRepartiment(tasques));
  const msWasm = mesurar('WebAssembly', () => millorRepartimentWasm(tasques));

  const estalviPerCrida = msJs - msWasm;
  const cridesPerAmortitzar = estalviPerCrida > 0 ? Math.ceil(msCarrega / estalviPerCrida) : Infinity;

  console.table({
    'Càrrega del mòdul (ms)': msCarrega.toFixed(1),
    'JavaScript (ms)': msJs.toFixed(1),
    'WebAssembly (ms)': msWasm.toFixed(1),
    'Acceleració': `${(msJs / msWasm).toFixed(2)}×`,
    'Crides per amortitzar la càrrega': cridesPerAmortitzar
  });

  return { msCarrega, msJs, msWasm };
}

Un resultat típic en un portàtil modern amb 11 tasques:

Mesura Valor
Càrrega i compilació del mòdul ~15 ms (+ 24 KB de descàrrega)
JavaScript ~48 ms
WebAssembly ~19 ms
Acceleració 2,5×
Crides necessàries per amortitzar la càrrega 1

L'acceleració és real. I ara la pregunta honesta: compensa per a Nómada Tasques?

Pregunta Resposta
És un càlcul pesat? Sí, 177.000 combinacions
Es crida sovint? No: la Marta planifica una vegada per trimestre
48 ms molesten l'usuari? No. Per sota de ~100 ms es percep com a instantani
Quant costa mantenir-ho? Un llenguatge més, una cadena de compilació més, un desplegament més
Hi ha alternativa més barata? Sí: un Web Worker, que evita el bloqueig sense sortir de JavaScript

Veredicte: no compensa. Estalviar 29 mil·lisegons en una operació trimestral no justifica afegir AssemblyScript al projecte. Si el càlcul creixés a 20 tasques (3²⁰ ≈ 3.500 milions de combinacions), ni Wasm no salvaria l'enfocament: caldria un algorisme millor —programació dinàmica o una heurística—, i aquesta és la lliçó de fons.

Abans d'optimitzar la tecnologia, optimitza l'algorisme. I abans d'optimitzar res, mesura. Un canvi d'O(3ⁿ) a O(n log n) supera qualsevol factor constant de 2,5×.

Aquella disciplina —mesurar primer, decidir després, amb criteris de cost i no d'entusiasme— és exactament el tema del Mòdul 9, i en particular de Mesurar Abans d'Optimitzar, on trobaràs les eines de perfilat que converteixen aquestes mesures casolanes en anàlisis serioses.

Errors Habituals i Consells

  • Creure que Wasm substitueix JavaScript. Conviuen, i Wasm ni tan sols toca el DOM sense passar per JavaScript.
  • Fer servir Wasm per accelerar manipulació del DOM. Serà més lent: cada operació creua la frontera.
  • Fer servir Wasm per amagar el codi. wasm2wat el descompila. No és protecció.
  • Creuar la frontera dins d'un bucle. 200.000 crides petites són més lentes que fer-ho tot en JavaScript. Una crida amb 200.000 elements és el correcte.
  • Servir el .wasm sense Content-Type: application/wasm. instantiateStreaming falla amb un error de MIME desconcertant.
  • Desar una vista tipada de vida llarga sobre la memòria. Si la memòria creix, l'ArrayBuffer queda desvinculat i la vista deixa de servir. Crea-la just abans de fer-la servir.
  • Oblidar alliberar memòria. En C/C++/Rust sense wasm-bindgen, el que reserves cal alliberar-ho: també hi ha fuites a Wasm.
  • Instanciar el mòdul a cada crida. Compilar costa. Instancia una vegada i reutilitza.
  • Ignorar la mida de descàrrega. Un mòdul de 300 KB per estalviar 20 ms és una pèrdua neta.
  • Mesurar sense escalfament. La primera execució de JavaScript no està optimitzada pel JIT i falseja la comparació a favor de Wasm.
  • Mesurar una sola vegada. Fes servir la mediana de diverses execucions.
  • Afegir Wasm sense pla de manteniment. Un altre llenguatge, una altra compilació, un altre desplegament, un altre coneixement a l'equip.
  • Consell: prova primer un Web Worker. Si el problema és que la interfície es congela, un worker ho resol sense sortir de JavaScript.
  • Consell: comença per AssemblyScript si vols experimentar. La sintaxi et resultarà familiar i veuràs el model sense aprendre Rust.
  • Consell: mira el .wat del que genera el teu compilador. És la millor manera d'entendre què està passant de debò.
  • Consell: a DevTools pots posar punts d'interrupció a Wasm. La depuració ha millorat molt; amb mapes de fonts pots fins i tot depurar el Rust original.
  • Consell: comprova disponibilitat (typeof WebAssembly === 'object') i tingues sempre la ruta en JavaScript com a reserva.

Exercicis

Exercici 1 — Carregar i fer servir un mòdul mínim. Escriu sumar.wat amb dues funcions exportades: sumar(a, b) que sumi dos i32, i factorial(n) que calculi el factorial de manera iterativa. Compila'l amb wat2wasm. Després escriu js/wasm/carregar.js amb una funció carregarModul(url) que faci servir instantiateStreaming amb reserva a arrayBuffer, comprovi la disponibilitat de WebAssembly i llanci un error clar si el servidor no envia el MIME correcte. Verifica que sumar(3, 4) dona 7 i factorial(10) dona 3.628.800.

Exercici 2 — Comparativa rigorosa. Escriu compararImplementacions({ nom, js, wasm, entrades }) que executi totes dues versions sobre diverses mides d'entrada, amb escalfament, mediana de set repeticions i comprovació que totes dues retornen el mateix resultat (una optimització que dona un resultat diferent no és una optimització). Ha de produir una taula amb console.table que inclogui, per cada mida: temps JS, temps Wasm, acceleració i si els resultats coincideixen. Afegeix la mida d'entrada a partir de la qual Wasm comença a compensar.

Exercici 3 — Quan NO fer servir Wasm. Sense escriure Wasm, escriu un informe raonat —en forma de funció avaluarWasm(perfil) que retorni una recomanació— que rebi { msEnJs, cridesPerSessio, kbDelModul, tipusDeFeina, existeixCodiNatiu, equipConeixRust } i retorni { recomanacio: 'si' | 'no' | 'potser', motius: [...] }. Ha d'aplicar les regles de la lliçó: descartar si la feina és DOM o E/S, descartar si el temps en JavaScript ja és imperceptible, calcular quantes sessions triga a amortitzar-se la descàrrega, i valorar el cost humà. Prova'l amb el perfil real de Nómada Tasques i amb el d'un editor d'imatge.

Solucions

Solució 1

;; sumar.wat
(module
  (func $sumar (param $a i32) (param $b i32) (result i32)
    local.get $a
    local.get $b
    i32.add)

  (func $factorial (param $n i32) (result i32)
    (local $resultat i32)
    (local $i i32)
    (local.set $resultat (i32.const 1))
    (local.set $i (i32.const 2))

    (block $fi
      (loop $bucle
        (br_if $fi (i32.gt_s (local.get $i) (local.get $n)))    ;; si i > n, sortir
        (local.set $resultat (i32.mul (local.get $resultat) (local.get $i)))
        (local.set $i (i32.add (local.get $i) (i32.const 1)))
        (br $bucle)))

    (local.get $resultat))

  (export "sumar" (func $sumar))
  (export "factorial" (func $factorial))
)
wat2wasm sumar.wat -o sumar.wasm
// js/wasm/carregar.js
export const HI_HA_WASM = typeof WebAssembly === 'object'
  && typeof WebAssembly.instantiate === 'function';

export async function carregarModul(url, importacions = {}) {
  if (!HI_HA_WASM) throw new Error('Aquest navegador no admet WebAssembly.');

  // Camí ràpid: compila mentre descarrega
  if (typeof WebAssembly.instantiateStreaming === 'function') {
    try {
      const { instance, module } = await WebAssembly.instantiateStreaming(fetch(url), importacions);
      return { exports: instance.exports, module };
    } catch (error) {
      if (error.message.includes('MIME')) {
        console.warn(`[wasm] El servidor no envia Content-Type: application/wasm per a ${url}. ` +
                     'Es fa servir el camí lent; corregeix-ho al servidor.');
      } else {
        console.warn('[wasm] instantiateStreaming ha fallat:', error.message);
      }
    }
  }

  // Reserva: descarregar sencer i compilar després
  const resposta = await fetch(url);
  if (!resposta.ok) throw new Error(`HTTP ${resposta.status} en carregar ${url}`);   // 07-02
  const bytes = await resposta.arrayBuffer();
  const { instance, module } = await WebAssembly.instantiate(bytes, importacions);
  return { exports: instance.exports, module };
}
const { exports } = await carregarModul('/wasm/sumar.wasm');
console.log(exports.sumar(3, 4));        // 7
console.log(exports.factorial(10));      // 3628800

El detall valuós és distingir l'error de MIME de qualsevol altra fallada: el missatge diu exactament què cal arreglar al servidor en lloc de deixar un TypeError opac. I fixa't en el factorial: amb i32 desborda a partir de 13, un recordatori que els tipus de Wasm són de mida fixa i no avisen en desbordar, a diferència dels Number de JavaScript.

Solució 2

// js/planificacio/comparar.js
function mesurarMediana(funcio, repeticions = 7) {
  funcio();                                           // escalfament del JIT
  const temps = [];
  for (let i = 0; i < repeticions; i += 1) {
    const t0 = performance.now();
    funcio();
    temps.push(performance.now() - t0);
  }
  temps.sort((a, b) => a - b);
  return temps[Math.floor(temps.length / 2)];
}

const gairebeIguals = (a, b, epsilon = 1e-9) =>
  typeof a === 'number' && typeof b === 'number'
    ? Math.abs(a - b) < epsilon
    : JSON.stringify(a) === JSON.stringify(b);

export function compararImplementacions({ nom, js, wasm, entrades }) {
  const files = {};
  let llindar = null;

  for (const entrada of entrades) {
    const resultatJs   = js(entrada.dades);
    const resultatWasm = wasm(entrada.dades);
    const coincideixen = gairebeIguals(resultatJs, resultatWasm);

    if (!coincideixen) {
      console.error(`❌ ${nom} · ${entrada.etiqueta}: resultats diferents`,
                    { js: resultatJs, wasm: resultatWasm });
    }

    const msJs   = mesurarMediana(() => js(entrada.dades));
    const msWasm = mesurarMediana(() => wasm(entrada.dades));
    const acceleracio = msJs / msWasm;

    if (llindar === null && acceleracio > 1.2) llindar = entrada.etiqueta;

    files[entrada.etiqueta] = {
      'JS (ms)': msJs.toFixed(2),
      'Wasm (ms)': msWasm.toFixed(2),
      'Acceleració': `${acceleracio.toFixed(2)}×`,
      'Coincideixen': coincideixen ? '✅' : '❌'
    };
  }

  console.table(files);
  console.log(llindar
    ? `Wasm comença a compensar a partir de: ${llindar}`
    : 'Wasm no compensa en cap mida provada.');
  return files;
}

La comprovació que els resultats coincideixen és la part que ningú escriu i que més falta fa: una versió més ràpida que retorna una altra cosa no és una optimització, és un bug. I el gairebeIguals amb epsilon reconeix una realitat dels decimals de coma flotant: JavaScript i Wasm poden diferir en l'últim bit en acumular sumes en ordre diferent.

Solució 3

export function avaluarWasm({
  msEnJs, cridesPerSessio, kbDelModul,
  tipusDeFeina,                   // 'calcul' | 'dom' | 'xarxa' | 'mixt'
  existeixCodiNatiu = false,
  equipConeixRust = false
}) {
  const motius = [];

  // 1 · Descarts immediats
  if (tipusDeFeina === 'dom' || tipusDeFeina === 'xarxa') {
    return { recomanacio: 'no', motius: [
      `La feina és de tipus "${tipusDeFeina}": Wasm no la pot accelerar i el cost de creuar la frontera l'empitjoraria.`
    ]};
  }

  if (msEnJs < 50) {
    motius.push(`${msEnJs} ms en JavaScript ja es percep com a instantani (llindar ~100 ms).`);
  }

  // 2 · Amortització de la descàrrega (a ~1 ms per KB en una connexió mitjana)
  const msDescarrega = kbDelModul * 1;
  const estalviEstimat = msEnJs * 0.6;                  // suposem una acceleració de 2,5×
  const cridesPerAmortitzar = Math.ceil(msDescarrega / estalviEstimat);

  motius.push(
    `El mòdul (${kbDelModul} KB ≈ ${msDescarrega} ms de descàrrega) s'amortitza després de ` +
    `${cridesPerAmortitzar} crides; la sessió típica en fa ${cridesPerSessio}.`
  );

  // 3 · Factors humans i de reutilització
  if (existeixCodiNatiu) motius.push("Ja existeix codi C/C++/Rust: reescriure'l seria el cost més gran.");
  if (!equipConeixRust && !existeixCodiNatiu) {
    motius.push("L'equip no domina un llenguatge compilable: hi ha cost d'aprenentatge i de manteniment.");
  }
  motius.push('Alternativa més barata: moure el càlcul a un Web Worker (continua sent JavaScript).');

  // 4 · Veredicte
  if (existeixCodiNatiu) return { recomanacio: 'si', motius };
  if (msEnJs < 50 || cridesPerSessio < cridesPerAmortitzar) {
    return { recomanacio: 'no', motius };
  }
  if (msEnJs > 200 && cridesPerSessio > cridesPerAmortitzar * 5) {
    return { recomanacio: 'si', motius };
  }
  return { recomanacio: 'potser', motius };
}
// El cas real de Nómada Tasques
console.log(avaluarWasm({
  msEnJs: 48, cridesPerSessio: 1, kbDelModul: 24,
  tipusDeFeina: 'calcul', existeixCodiNatiu: false, equipConeixRust: false
}));
// { recomanacio: 'no', motius: [ '48 ms en JavaScript ja es percep com a instantani…', … ] }

// Un editor d'imatge al navegador
console.log(avaluarWasm({
  msEnJs: 2400, cridesPerSessio: 300, kbDelModul: 180,
  tipusDeFeina: 'calcul', existeixCodiNatiu: true, equipConeixRust: true
}));
// { recomanacio: 'si', motius: [ 'Ja existeix codi C/C++/Rust…', … ] }

L'interessant d'aquesta funció no és el codi, sinó que obliga a escriure els números abans de decidir. La majoria de les decisions de «farem servir Wasm» es prenen per entusiasme tecnològic i no sobreviuen a omplir honestament aquests sis camps.

Conclusió

Tanques el Mòdul 7 amb la lliçó més contraintuïtiva de totes: la que ensenya una tecnologia i acaba recomanant no fer-la servir. Saps que WebAssembly és un format binari portable i segur que s'executa al mateix motor que JavaScript, i que no el substitueix: conviuen, es criden mútuament i Wasm ni tan sols pot tocar el DOM sense que li passis una funció JavaScript com a importació. Saps desmuntar els quatre mites —no és més ràpid en tot, no amaga el codi (wasm2wat el descompila), no accedeix al sistema de fitxers al navegador, i no és un llenguatge sinó una destinació de compilació—. I coneixes les dues raons reals per les quals existeix: el rendiment predictible, sense les desoptimitzacions especulatives del JIT, i la possibilitat de reutilitzar dècades de codi en C, C++ i Rust.

Entens el model d'execució: mòdul com a classe i instància com a objecte, la memòria lineal que és literalment un ArrayBuffer compartit amb JavaScript, la taula de funcions, i els quatre únics tipus numèrics. D'aquí surt la idea que governa qualsevol integració: Wasm només entén números, les cadenes i els objectes cal serialitzar-los a bytes, i per tant cal creuar la frontera poques vegades amb molta feina, mai al revés. Saps llegir un .wat com una màquina de pila, carregar un mòdul amb instantiateStreaming —amb la seva exigència de Content-Type: application/wasm i la seva reserva per arrayBuffer—, exportar i importar, i compartir memòria amb vistes tipades creades just abans de fer-les servir, perquè si la memòria creix el buffer anterior queda desvinculat.

Coneixes les eines de debò —wasm-pack amb wasm-bindgen per a Rust, Emscripten per a C i C++, AssemblyScript com a porta d'entrada des de TypeScript—, els casos on Wasm ha guanyat de manera indiscutible (Figma, ffmpeg.wasm, SQLite, Pyodide, motors de joc, CAD) i la comparativa honesta que gairebé ningú fa: la mida de descàrrega, el cost de la frontera i el cost humà de mantenir dos llenguatges i dues cadenes de compilació. I sobretot, has fet l'exercici complet amb la planificació del Taller Nómada: una simulació real de 177.000 combinacions, una versió en JavaScript, una altra en AssemblyScript, una mesura amb escalfament i mediana… i un veredicte raonat de no compensa, perquè estalviar 29 mil·lisegons en una operació trimestral no justifica afegir un llenguatge al projecte, i perquè quan el problema creix de debò la resposta no és canviar de tecnologia sinó canviar d'algorisme.

Amb això acaba el Mòdul 7. Nómada Tasques ha recorregut un camí complet: recorda entre recàrregues amb la Web Storage API i el seu repositori local; parla amb un servidor mitjançant fetch, comprovant sempre resposta.ok; sobreviu a la xarxa real amb ErrorDeApi, AbortController, timeouts, reintents amb retrocés exponencial i una interfície de quatre estats; se sincronitza en directe amb un CanalTauler sobre WebSockets que es reconnecta sol, bat i encua el que està pendent; funciona sense connexió i instal·lada gràcies a un service worker amb el seu app shell precarregat i les seves estratègies per tipus de recurs; i es comporta com una aplicació acurada, amb filtres enllaçables, tema respectuós amb el sistema, animacions que atenen prefers-reduced-motion i formats en català de debò amb Intl. La capa js/dades/ que va començar buida té ara quatre mòduls, i el model dels Mòduls 1 a 5 no ha canviat ni una línia en tot el procés.

I aquí hi ha exactament el problema que obre el mòdul següent. L'aplicació fa ja moltíssimes coses: sis capes, catorze mòduls, dues fonts de dades, un canal en temps real, un proxy de memòria cau, regles de negoci, estats d'interfície, reversions optimistes i cues de sincronització. Cadascuna d'aquelles peces pot trencar les altres, i cap comprovació no es fa sola. Ara mateix, l'única manera de saber si alguna cosa continua funcionant és obrir-la i provar-la a mà; i quan falla, l'única manera d'esbrinar per què és sembrar el codi de console.log. Això no escala: arriba un punt en què canviar una línia fa por. Cal depurar amb mètode en lloc de per intuïció, mantenir el codi consistent amb eines automàtiques, i sobretot automatitzar les proves perquè la màquina comprovi en segons el que tu no pots comprovar en una tarda. Aquest és el Mòdul 8: Proves i Depuració, que comença per Depuració de JavaScript —i on comprovaràs que aquella frontera neta entre model i vista, la que portes sis mòduls mantenint, era des del principi el que faria possible provar-ho tot sense obrir un navegador—.

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