Nómada Tareas funciona y está probada: 124 pruebas en cuatro segundos y tres recorridos E2E confirman que hace lo que debe. Pero Taller Nómada lleva dos años usándola y el tablero ya no tiene seis tareas: tiene seiscientas. Marta dice que «va lenta». Iván dice que «al escribir en el buscador se atasca». Lucía dice que «tarda mucho en cargar». Tres frases, tres problemas posiblemente distintos, y ninguna de las tres es accionable. Antes de tocar una sola línea hay que convertir «va lenta» en un número, ese número en un objetivo, y ese objetivo en una prueba que falle cuando se incumpla. Esta lección enseña esa disciplina: qué significa «rápido» para una persona, qué métricas lo capturan (los Web Vitals), con qué herramientas se obtienen (Lighthouse, el panel Performance, el panel Network, la User Timing API), cómo se hace una medición honesta que no se deja engañar por el ruido ni por la máquina del desarrollador, y cómo se fija un presupuesto de rendimiento que la integración continua vigile sola. Al terminar tendrás la línea base de Nómada Tareas con 600 tareas: la tabla de números que las cuatro lecciones siguientes van a mejorar.

Contenido

  1. Por qué la intuición falla (y qué dijo Knuth de verdad)
  2. Qué percibe una persona: 100 ms, 1 s y 16,7 ms
  3. Los Web Vitals, uno a uno
  4. Datos de laboratorio y datos de campo
  5. Lighthouse: leer el informe sin obsesionarse con la puntuación
  6. El panel Performance, paso a paso
  7. El panel Network y el estrangulamiento de red y CPU
  8. Instrumentar tu propio código: performance.now() y la User Timing API
  9. Medir en producción: PerformanceObserver y web-vitals
  10. Cómo se hace un benchmark honesto
  11. Tu máquina miente: simular un móvil modesto
  12. La línea base de Nómada Tareas con 600 tareas
  13. El presupuesto de rendimiento y la integración continua
  14. Errores Comunes y Consejos
  15. Ejercicios
  16. Conclusión

  1. Por qué la intuición falla (y qué dijo Knuth de verdad)

En 08-01 aprendiste que depurar por intuición no converge: cambias cosas, algunas parecen funcionar, y acabas con un código peor y un error que sigue ahí. El rendimiento es idéntico, pero peor, porque el error al menos se manifiesta y la lentitud se puede negar («a mí me va bien»).

Hay una frase que se cita constantemente y casi siempre mal:

«La optimización prematura es la raíz de todos los males.»

La frase es de Donald Knuth, en Structured Programming with go to Statements (1974), y completa dice algo bastante distinto:

«Deberíamos olvidarnos de las pequeñas eficiencias, digamos el 97 % del tiempo: la optimización prematura es la raíz de todos los males. Pero no deberíamos dejar pasar nuestras oportunidades en ese 3 % crítico.»

Y unas líneas antes, el párrafo que nadie cita y que es el que importa aquí:

«A menudo es un error hacer juicios a priori sobre qué partes de un programa son realmente críticas, ya que la experiencia universal de los programadores que han usado herramientas de medición es que sus suposiciones intuitivas fallan

Knuth no estaba diciendo «no optimices». Estaba diciendo «no optimices lo que no has medido», que es exactamente lo contrario de como se suele usar la cita. Su recomendación explícita era instrumentar el programa, encontrar el 3 % que consume el tiempo y trabajar ahí con todo el cuidado del mundo.

¿Por qué falla la intuición? Por tres motivos concretos:

Motivo Ejemplo en Nómada Tareas
El coste no está donde parece El sort de 600 tareas cuesta 0,4 ms; pintar sus 600 tarjetas cuesta 310 ms. Todo el mundo mira el sort
Los órdenes de magnitud engañan Cambiar forEach por for ahorra microsegundos; quitar un getBoundingClientRect de un bucle ahorra 400 ms
El cuello de botella se mueve Al arreglar el render, el problema pasa a ser la descarga; optimizar el render otra vez ya no aporta nada

La consecuencia práctica es una regla de trabajo que vas a aplicar en todo el módulo:

flowchart LR
    A["1 · Definir<br/>qué es rápido"] --> B["2 · Medir<br/>la línea base"]
    B --> C["3 · Encontrar<br/>el cuello de botella"]
    C --> D["4 · Cambiar<br/>UNA cosa"]
    D --> E["5 · Medir<br/>otra vez"]
    E -->|"mejora"| F["6 · Fijar<br/>el presupuesto"]
    E -->|"no mejora"| G["Revertir"]
    G --> C
    F --> C

El paso 4 —cambiar una sola cosa— es el que casi todo el mundo se salta, y es el mismo que en depuración: si cambias cinco cosas y mejora, no sabes cuál sirvió, y probablemente cuatro de ellas solo han añadido complejidad. El paso Revertir tampoco es opcional: una optimización que no mejora es deuda técnica pura.

  1. Qué percibe una persona: 100 ms, 1 s y 16,7 ms

«Rápido» no es un número absoluto: es lo que el sistema perceptivo humano tolera. Hay tres umbrales que conviene tener memorizados porque gobiernan todas las decisiones del módulo.

Umbral Qué significa Qué ocurre si se supera
~100 ms Límite de la sensación de respuesta instantánea a una acción propia El usuario percibe que la aplicación «reacciona», no que «responde»
~1 s Límite para mantener el hilo de pensamiento Se nota la espera, pero no se abandona la tarea mental
~10 s Límite de la atención El usuario cambia de pestaña, y hay que mostrar progreso explícito
16,7 ms Presupuesto de un fotograma a 60 Hz Se pierde el fotograma: la animación o el desplazamiento «salta»

El de 16,7 ms merece explicación. Una pantalla a 60 Hz se refresca 60 veces por segundo: 1000 / 60 = 16,7 ms por fotograma. En ese tiempo el navegador tiene que ejecutar tu JavaScript, recalcular estilos, hacer el layout, pintar y componer. Si tu manejador de scroll tarda 20 ms, el fotograma no llega a tiempo y se pierde. Y en pantallas de 120 Hz el presupuesto baja a 8,3 ms.

Como el navegador necesita parte de ese tiempo para lo suyo, la regla práctica es dejar menos de 10 ms de JavaScript por fotograma cuando algo se está moviendo.

flowchart TD
    subgraph F["Un fotograma a 60 Hz: 16,7 ms"]
        direction LR
        J["JS<br/>~10 ms"] --> S["Estilo"] --> L["Layout"] --> P["Pintado"] --> C["Composición"]
    end
    F --> OK["Fotograma entregado a tiempo"]
    X["JS de 20 ms"] --> KO["Fotograma perdido:<br/>salto visible"]

Traducido a Nómada Tareas, esto ya da tres objetivos concretos antes incluso de medir nada:

  • Pulsar «Empezar» en una tarjeta debe verse reflejado en menos de 100 ms.
  • El tablero debe estar visible en menos de 1 s con conexión decente.
  • Desplazar la columna de pendientes no debe gastar más de 10 ms de JavaScript por fotograma.

Fíjate en el cambio de lenguaje: hemos pasado de «va lenta» a tres afirmaciones falsables. Eso ya es la mitad del trabajo.

  1. Los Web Vitals, uno a uno

Los umbrales del apartado anterior son psicología. Los Web Vitals son la forma estandarizada de medirlos en una página real: un conjunto pequeño de métricas definidas por Google que capturan carga, interactividad y estabilidad visual. Son las que miden Lighthouse, PageSpeed Insights, Search Console y prácticamente cualquier herramienta de monitorización.

Hay tres principales (los Core Web Vitals) y dos complementarios que sirven para diagnosticar.

Métrica Qué mide Bueno Mejorable Malo Qué la empeora típicamente
LCP (Largest Contentful Paint) Cuándo se pinta el elemento de contenido más grande del área visible ≤ 2,5 s 2,5–4 s > 4 s Servidor lento, CSS y JS que bloquean, imágenes pesadas o sin priorizar, fuentes
INP (Interaction to Next Paint) Latencia de las interacciones: desde la pulsación hasta el siguiente pintado ≤ 200 ms 200–500 ms > 500 ms Manejadores pesados, tareas largas, render completo en cada evento
CLS (Cumulative Layout Shift) Cuánto se mueve el contenido sin que el usuario lo provoque ≤ 0,1 0,1–0,25 > 0,25 Imágenes sin dimensiones, fuentes que cambian de tamaño, banners insertados
TTFB (Time to First Byte) Cuándo llega el primer byte del documento ≤ 800 ms 0,8–1,8 s > 1,8 s Servidor, base de datos, redirecciones, falta de caché o CDN
FCP (First Contentful Paint) Cuándo aparece el primer contenido (cualquiera) ≤ 1,8 s 1,8–3 s > 3 s Recursos que bloquean el renderizado, sobre todo CSS

Cuatro precisiones que evitan la mayoría de los malentendidos:

LCP no es «cuándo carga la página». Es cuándo aparece el elemento más grande del área visible: normalmente la imagen de cabecera, un bloque de texto grande o —en Nómada Tareas— el contenedor .tablero con las primeras tarjetas. Si tu aplicación pinta un esqueleto gris rapidísimo y las tarjetas reales tardan tres segundos, el LCP será malo, y con razón: el esqueleto no es contenido.

INP sustituyó a FID. La métrica antigua, First Input Delay, medía solo el retraso hasta que empezaba a ejecutarse el manejador de la primera interacción; era demasiado benévola. INP mide todas las interacciones de la sesión, de principio a fin —retraso de entrada, ejecución del manejador y pintado del resultado— y se queda con (aproximadamente) la peor. Es la métrica que Nómada Tareas va a suspender: escribir en el buscador dispara un render completo de 600 tarjetas.

CLS no tiene unidad. Es un producto de la fracción de pantalla afectada por la distancia recorrida, acumulado en ventanas de sesión. Un 0,21 significa «el contenido salta de forma claramente perceptible». En Nómada Tareas el culpable es previsible: las tarjetas se insertan cuando llegan los datos y empujan hacia abajo lo que había.

TTFB y FCP no son objetivos, son diagnóstico. No se optimizan por sí mismos: se miran para saber dónde está el problema de LCP. Si TTFB es 1,4 s de los 4,1 s de LCP, el problema es del servidor y no vas a arreglarlo tocando JavaScript.

flowchart LR
    A["Petición"] -->|"TTFB"| B["Primer byte"]
    B -->|"analizar HTML,<br/>CSS bloqueante"| C["FCP<br/>primer contenido"]
    C -->|"JS, datos,<br/>imágenes"| D["LCP<br/>contenido principal"]
    D --> E["Interacción<br/>del usuario"]
    E -->|"INP"| F["Siguiente pintado"]

Una advertencia importante: los Web Vitals miden la experiencia, no la calidad de tu código. Una página con un LCP de 1,2 s puede tener un JavaScript horrible, y una página impecable puede suspender por culpa de una fuente. Son la primera pregunta, no la última.

  1. Datos de laboratorio y datos de campo

Vas a ver dos veces la misma métrica con valores completamente distintos, y conviene entender por qué antes de volverte loco.

Laboratorio (lab data) Campo (field data / RUM)
Cómo se obtiene Ejecutas Lighthouse o el panel Performance en tu máquina Recoges métricas de usuarios reales en producción
Entorno Controlado, repetible, simulado Caótico: móviles viejos, 3G, pestañas de fondo, extensiones
Ventaja Reproducible; se puede meter en CI; permite comparar A/B Es la verdad: lo que de hecho le pasa a la gente
Inconveniente No es la realidad No reproducible; llega con retraso; necesita volumen
INP Se estima; nadie interactúa de verdad Se mide de verdad
Se resume con Una ejecución (o la mediana de varias) El percentil 75 de todas las sesiones

Que se use el percentil 75 es la clave de por qué no coinciden. Cuando Search Console dice que tu LCP es de 3,8 s, no dice que el usuario medio espere 3,8 s: dice que el 25 % de las visitas esperan más de eso. Tu Lighthouse local puede marcar 1,9 s sin mentir; simplemente estás midiendo el percentil de un usuario con fibra, un portátil potente y la caché caliente.

La regla operativa:

  • Campo para saber si hay un problema y a quién le pasa.
  • Laboratorio para encontrar la causa y para verificar que un cambio la arregla.

Usar solo laboratorio lleva a optimizar problemas que nadie tiene. Usar solo campo lleva a saber que algo va mal sin poder averiguar qué.

  1. Lighthouse: leer el informe sin obsesionarse con la puntuación

Lighthouse está integrado en DevTools (pestaña Lighthouse), en la línea de comandos (npx lighthouse) y en PageSpeed Insights. Genera un informe de laboratorio con cinco categorías; aquí solo nos interesa Performance.

# Informe en la terminal, útil para integración continua
npx lighthouse http://localhost:5173 \
  --preset=desktop \
  --output=json --output-path=./informes/lh-escritorio.json

# Perfil móvil (el que importa): CPU 4× más lenta y red lenta simulada
npx lighthouse http://localhost:5173 \
  --output=html --output-path=./informes/lh-movil.html

Por defecto Lighthouse simula un móvil de gama media: estrangula la CPU a 4× más lenta que la tuya y aplica una red lenta. Eso explica por qué la puntuación móvil siempre sale mucho peor que la de escritorio, y por qué la móvil es la que hay que mirar.

Ahora, la parte importante: cómo leer el informe.

  1. Ignora el número grande. La puntuación de 0 a 100 es una media ponderada de las métricas, comprimida en una curva log-normal. Subir de 50 a 60 puede ser un cambio enorme o un ruido de medición; y perseguir el 100 lleva a decisiones absurdas. Lo que importa son las métricas individuales.
  2. Lee las métricas en la parte superior, con sus colores. Ahí están FCP, LCP, TBT, CLS y Speed Index.
  3. TBT (Total Blocking Time) es tu sustituto de INP en laboratorio. Suma, de todas las tareas largas, los milisegundos que exceden de 50 ms. Es lo único que Lighthouse puede decirte sobre interactividad sin que haya un usuario interactuando. Si el TBT es alto, el INP de campo va a ser malo.
  4. Baja a Diagnostics y a las oportunidades. Ahí están las acciones concretas, con el ahorro estimado. Trátalas como pistas, no como órdenes: el «ahorro estimado» es un modelo, no una medición.
  5. Busca el elemento LCP. El informe lo señala explícitamente. Saber qué elemento es cambia por completo el diagnóstico.
  6. Ejecútalo tres veces. Lighthouse tiene una varianza notable. Una sola ejecución no es un dato.
  7. Ejecútalo en modo incógnito y sin extensiones. Las extensiones inyectan scripts y falsean el resultado; es el error de medición más común.

Un extracto real del informe de Nómada Tareas con 600 tareas (perfil móvil, tres ejecuciones, mediana):

Métrica de Lighthouse Valor Veredicto
First Contentful Paint 2,3 s Mejorable
Largest Contentful Paint 4,1 s Malo
Total Blocking Time 1.240 ms Malo
Cumulative Layout Shift 0,21 Mejorable
Speed Index 3,9 s Mejorable
Puntuación 38 (irrelevante por sí sola)

Ese TBT de 1.240 ms es la señal más fuerte del informe: hay más de un segundo en el que el hilo principal está ocupado y la interfaz no responde. Lighthouse no te dice por qué. Para eso está el panel siguiente.

  1. El panel Performance, paso a paso

El panel Performance de DevTools es la herramienta central del módulo. Graba todo lo que hace el navegador durante un intervalo y lo presenta en una línea de tiempo. Es intimidante la primera vez; con un procedimiento fijo deja de serlo.

Procedimiento de grabación

  1. Abre la aplicación en una ventana de incógnito (sin extensiones).
  2. Abre DevTools → pestaña Performance.
  3. Marca Screenshots y activa el estrangulamiento: CPU: 4× slowdown y, si la carga te interesa, Network: Slow 4G.
  4. Para medir la carga: pulsa el botón de recarga con círculo (Start profiling and reload page). Para medir una interacción: pulsa el círculo de grabar, haz la interacción, y para en cuanto acabe.
  5. Graba poco. Tres segundos de grabación son analizables; treinta, no.

Cómo leer lo grabado, de arriba abajo:

Zona Qué contiene Para qué la usas
Capturas de pantalla Fotogramas reales Ver cuándo aparece el contenido
Web Vitals Marcas de FCP, LCP, DCL, Load Anclar las métricas en la línea de tiempo
Frames Un rectángulo por fotograma; rojo = perdido Diagnosticar desplazamientos y animaciones
Main El hilo principal: el diagrama de llamas Donde está el 90 % de las respuestas
Network Cascada de peticiones Ver qué bloquea qué
Sumario / Bottom-Up / Call Tree Agregados de la selección Cuantificar

El diagrama de llamas (flame chart) del carril Main se lee así: el eje horizontal es el tiempo; cada barra es una llamada a función; una barra debajo de otra es una función llamada por la de arriba. La anchura es el tiempo total (propio + descendientes). Los colores tienen significado fijo:

Color Categoría
Amarillo Scripting (tu JavaScript)
Morado Rendering (cálculo de estilo y layout)
Verde Painting (pintado y composición)
Azul Loading (analizar HTML y CSS)
Gris Otros / sistema

Las tareas largas son lo primero que hay que buscar. Una tarea es una unidad de trabajo que el bucle de eventos (05-07) ejecuta sin interrupción; si dura más de 50 ms se considera larga y DevTools la marca con un triángulo rojo en la esquina y una franja roja rayada. Mientras dura, el navegador no puede atender clics ni pintar. Esa es la traducción visual exacta de lo que estudiaste en 05-07: un bucle pesado congela la interfaz, y await no lo arregla.

Bottom-Up frente a Call Tree. Selecciona un intervalo en la línea de tiempo y mira las pestañas de abajo:

Pestaña Agrupa por Responde a
Call Tree Cadena de llamadas desde la raíz «¿Qué provocó este trabajo?»
Bottom-Up Función hoja, sumando todas sus apariciones «¿Qué función consume más tiempo propio
Event Log Orden cronológico de eventos «¿Qué pasó exactamente y en qué orden?»

La distinción self time / total time es la que más confunde al principio. Total incluye lo que tarda todo lo que la función llama; self es solo el tiempo dentro de su propio cuerpo. render() puede tener un total de 310 ms y un self de 2 ms: no es que render sea lenta, es que llama a algo que lo es. Bottom-Up ordenado por self time es la vista que encuentra al culpable.

Al abrir la primera grabación de Nómada Tareas con 600 tareas, ordenando Bottom-Up por self time, aparece esto:

Función Self time Total Apariciones
pintarTarjeta 186 ms 218 ms 600
Recalculate Style 74 ms 74 ms 41
Layout 63 ms 63 ms 38
#visibles 21 ms 24 ms 23
Tablero.resumen 18 ms 96 ms 69

Eso ya no es «va lenta»: es «600 llamadas a pintarTarjeta cuestan 186 ms de tiempo propio, y hay 41 recálculos de estilo donde debería haber uno». Las lecciones 09-02 y 09-04 atacan exactamente esas filas.

  1. El panel Network y el estrangulamiento de red y CPU

Ya usaste Network en 08-01 para depurar peticiones. Aquí lo usas para otra cosa: saber cuánto se descarga, en qué orden y qué está bloqueando el renderizado.

Cuatro columnas que hay que mirar siempre:

Columna Qué te dice
Size Dos valores: transferido (comprimido) y real. Si coinciden, no hay compresión
Time Duración total, con el desglose en la pestaña Timing
Priority Cómo prioriza el navegador ese recurso (Highest, High, Low)
Waterfall La cascada: qué espera a qué

Y tres ajustes de la barra superior:

  • Disable cache: obligatorio para medir la primera visita. Sin esto mides tu caché, no la de tus usuarios.
  • Throttling de red: Slow 4G es el perfil realista para móvil.
  • Estrangulamiento de CPU: está en la pestaña Performance y en Rendering; 4× slowdown aproxima un móvil de gama media, uno modesto.

Al pie del panel, la barra de resumen da el dato que importa para 09-05: peticiones, kB transferidos, kB de recursos y tiempos de DOMContentLoaded y Load. Para Nómada Tareas, con módulos ES sin empaquetar (05-04), esa barra dice hoy:

31 requests · 238 kB transferred · 512 kB resources · DOMContentLoaded 1,9 s · Load 4,3 s

Treinta y una peticiones porque cada módulo ES es un fichero (07-05 ya lo avisó al escribir la lista de precaché). Ese número es un problema de 09-05, no de hoy; hoy solo lo anotamos.

  1. Instrumentar tu propio código: performance.now() y la User Timing API

Las herramientas anteriores miden la página. Para medir tu código —una función concreta, una fase concreta— hay que instrumentarlo.

8.1 performance.now()

Date.now() devuelve milisegundos enteros desde 1970 y puede saltar hacia atrás si el reloj del sistema se ajusta. performance.now() devuelve milisegundos desde que se abrió la página, es monótono (nunca retrocede) y tiene resolución de fracción de milisegundo (reducida deliberadamente por seguridad: en la práctica, granularidad de decenas de microsegundos).

const inicio = performance.now();
vista.render();
console.log(`render: ${(performance.now() - inicio).toFixed(1)} ms`);

8.2 performance.mark y performance.measure

console.time/console.timeEnd valen para un vistazo rápido, pero solo salen por consola. La User Timing API hace algo mucho mejor: las medidas aparecen en el panel Performance, en un carril propio (Timings), alineadas con el diagrama de llamas.

// js/util/medicion.js

/** Marca el inicio de una fase con nombre. */
export function inicio(nombre) {
  performance.mark(`${nombre}:inicio`);
}

/**
 * Cierra la fase, crea la medida (visible en el panel Performance)
 * y devuelve su duración en milisegundos.
 */
export function fin(nombre) {
  performance.mark(`${nombre}:fin`);
  const medida = performance.measure(nombre, `${nombre}:inicio`, `${nombre}:fin`);

  // Limpieza: si no borras las marcas, el búfer crece durante toda la sesión
  performance.clearMarks(`${nombre}:inicio`);
  performance.clearMarks(`${nombre}:fin`);
  performance.clearMeasures(nombre);

  return medida.duration;
}

/** Envuelve una función para medirla sin ensuciar su cuerpo. */
export function medido(nombre, fn) {
  return function (...args) {
    inicio(nombre);
    try {
      return fn.apply(this, args);
    } finally {
      console.log(`${nombre}: ${fin(nombre).toFixed(1)} ms`);   // finally: mide aunque lance
    }
  };
}

Tres detalles del código que conviene entender:

  • performance.measure devuelve el objeto PerformanceMeasure desde hace años, así que no hace falta buscarlo con getEntriesByName.
  • El finally garantiza que la medición se cierra aunque la función lance una excepción; sin él, una marca huérfana rompe la siguiente medida.
  • La limpieza importa: el búfer de entradas de rendimiento tiene un tamaño limitado y las marcas viejas desplazan a las nuevas.

Instrumentando TableroVista.render con esto y grabando con el panel abierto, el carril Timings muestra una barra render de 310 ms perfectamente alineada con el bloque amarillo del hilo principal. Ya no hay que adivinar qué parte del diagrama de llamas es tuya.

// js/vista/tablero-vista.js — instrumentación temporal
import { inicio, fin } from '../util/medicion.js';

render() {
  inicio('render');
  const visibles = this.#visibles();

  inicio('render:columnas');
  // …reconciliar las tres columnas…
  fin('render:columnas');

  inicio('render:resumen');
  const r = this.#estado.tablero.resumen(this.#estado.hoy);
  // …
  fin('render:resumen');

  fin('render');
}

Con fases anidadas obtienes el desglose sin tener que interpretar el diagrama de llamas:

Fase Duración (mediana de 9)
render 310 ms
├─ render:columnas 291 ms
└─ render:resumen 4,2 ms

Deja la instrumentación fuera de producción o detrás de una bandera. Las marcas cuestan poco, pero los console.log dentro de bucles cuestan muchísimo, y además falsean la propia medición.

  1. Medir en producción: PerformanceObserver y web-vitals

Para el dato de campo hay que recoger métricas de usuarios reales. La API base es PerformanceObserver, que notifica entradas de rendimiento según se producen.

// js/util/vitales.js — versión artesanal, para entender el mecanismo

// Tareas largas: cualquier bloque de más de 50 ms sin ceder el hilo
new PerformanceObserver((lista) => {
  for (const entrada of lista.getEntries()) {
    console.warn(`Tarea larga: ${entrada.duration.toFixed(0)} ms`, entrada.attribution);
  }
}).observe({ type: 'longtask', buffered: true });

// LCP: llega varias veces; vale la ÚLTIMA antes de la primera interacción
let lcp = 0;
new PerformanceObserver((lista) => {
  const entradas = lista.getEntries();
  lcp = entradas[entradas.length - 1].startTime;
}).observe({ type: 'largest-contentful-paint', buffered: true });

Dos detalles nada obvios:

  • buffered: true entrega también las entradas ocurridas antes de crear el observador. Sin eso te pierdes el LCP, que suele suceder antes de que tu script se ejecute.
  • El LCP cambia a medida que se pinta contenido mayor. El valor definitivo es el último antes de que el usuario interactúe o la página se oculte.

Implementar bien LCP, INP y CLS a mano es sorprendentemente delicado (ventanas de sesión de CLS, agrupación de eventos de INP, páginas restauradas desde la caché de retroceso…). Para producción se usa la librería oficial web-vitals, que pesa poco más de 2 kB:

npm install web-vitals
// js/util/vitales.js — versión de producción
import { onLCP, onINP, onCLS, onTTFB, onFCP } from 'web-vitals';

/**
 * Envía una métrica al servidor. `sendBeacon` sobrevive a la descarga
 * de la página, cosa que un fetch normal no garantiza.
 */
function enviar(metrica) {
  const cuerpo = JSON.stringify({
    nombre: metrica.name,          // 'LCP' | 'INP' | 'CLS' | 'TTFB' | 'FCP'
    valor: metrica.value,
    calificacion: metrica.rating,  // 'good' | 'needs-improvement' | 'poor'
    id: metrica.id,                // identifica la visita
    ruta: location.pathname
  });

  navigator.sendBeacon?.('/api/vitales', cuerpo)
    ?? fetch('/api/vitales', { body: cuerpo, method: 'POST', keepalive: true });
}

onLCP(enviar);
onINP(enviar);
onCLS(enviar);
onTTFB(enviar);
onFCP(enviar);

Con eso, y agregando en el servidor por percentil 75, Taller Nómada tendría el dato de campo. Recuerda: CLS e INP solo se conocen del todo al final de la visita, por eso sendBeacon (que la API de fetch con keepalive imita) es la forma correcta de enviarlos.

  1. Cómo se hace un benchmark honesto

En 07-07 ya escribiste una función medir para comparar JavaScript con WebAssembly, y ya usaste dos de las reglas: calentamiento y mediana. Toca formalizarlas, porque el resto del módulo depende de que tus mediciones signifiquen algo.

Regla Por qué Qué pasa si la incumples
Calentar antes de medir El JIT (09-02) necesita varias ejecuciones para optimizar La primera ejecución es 3–10× más lenta: comparación inválida
Repetir (≥ 7 veces) Una medición es una anécdota Mides ruido del sistema
Usar la mediana, no la media Una pausa del recolector de basura dispara la media Un valor atípico decide tu conclusión
Mirar también el mínimo Es el caso «sin interferencias» No distingues código lento de máquina ocupada
Comparar contra una referencia Los números absolutos no se transfieren entre máquinas «42 ms» no significa nada sin un «frente a qué»
Medir una sola variable Si cambian dos cosas, no sabes cuál actuó Conclusión no atribuible
Cerrar lo demás Otras pestañas, compilaciones, antivirus Ruido enorme e irregular

Aquí está la utilidad completa que usaremos en todo el módulo:

// js/util/banco.js

/**
 * Ejecuta `fn` repetidas veces y devuelve estadísticas robustas.
 * @param {string} nombre    Etiqueta para el informe.
 * @param {Function} fn      Función a medir. Debe ser autocontenida.
 * @param {object} opciones
 * @param {number} opciones.calentamiento  Ejecuciones descartadas (JIT).
 * @param {number} opciones.repeticiones   Ejecuciones medidas.
 */
export function banco(nombre, fn, { calentamiento = 5, repeticiones = 15 } = {}) {
  for (let i = 0; i < calentamiento; i += 1) fn();      // 1 · calentar: no se mide

  const tiempos = [];
  for (let i = 0; i < repeticiones; i += 1) {           // 2 · medir
    const t0 = performance.now();
    fn();
    tiempos.push(performance.now() - t0);
  }

  tiempos.sort((a, b) => a - b);                        // 3 · estadísticas robustas
  const p = (q) => tiempos[Math.min(tiempos.length - 1, Math.floor(tiempos.length * q))];

  return {
    nombre,
    min: tiempos[0],
    mediana: p(0.5),
    p95: p(0.95),
    max: tiempos.at(-1)
  };
}

/** Compara dos alternativas sobre la misma entrada e informa de la mejora. */
export function comparar(a, b, opciones) {
  const ra = banco(a.nombre, a.fn, opciones);
  const rb = banco(b.nombre, b.fn, opciones);
  console.table([ra, rb].map((r) => ({
    Variante: r.nombre,
    'Mediana (ms)': r.mediana.toFixed(2),
    'Mín (ms)': r.min.toFixed(2),
    'p95 (ms)': r.p95.toFixed(2)
  })));
  console.log(`Mejora: ${(ra.mediana / rb.mediana).toFixed(2)}×`);
  return { a: ra, b: rb };
}

El p95 merece un comentario: la mediana dice cómo va normalmente, el p95 dice cómo va en el 5 % de peores casos. Para el rendimiento percibido, el p95 a menudo importa más, porque es el que produce el tirón que el usuario recuerda.

Y una regla de oro que ahorra discusiones enteras: si la diferencia entre dos variantes es menor que la variabilidad de tu propia medición, no hay diferencia. Si la mediana es 12 ms y el rango va de 9 a 18 ms, una «mejora» a 11,4 ms es ruido.

  1. Tu máquina miente: simular un móvil modesto

Tu portátil de desarrollo tiene un procesador rápido, mucha RAM, disco SSD, conexión por cable y la caché caliente. Tus usuarios, no. La diferencia entre un portátil de desarrollo y un móvil de gama media típico está en torno a 4–6× en CPU de un solo hilo, y es todavía mayor en un móvil de gama baja de hace cuatro años.

Traducido: esos 310 ms de render() que mides tú son más de un segundo y medio en el móvil desde el que Iván consulta el tablero cuando está fuera del taller.

Cómo simularlo:

Qué simular Dónde Ajuste recomendado
CPU lenta Performance → CPU: 4× slowdown 4× para gama media, 6× para gama baja
Red lenta Network → Throttling Slow 4G
Pantalla pequeña Device toolbar (Ctrl+Shift+M) Un móvil concreto de la lista
Todo a la vez Lighthouse, perfil móvil Es el valor por defecto

Dos avisos honestos sobre el estrangulamiento de CPU: es una simulación (ralentiza la ejecución de forma uniforme, mientras que un móvil real también tiene menos caché, memoria más lenta y limitación térmica), y no ralentiza la red ni el disco. Es una aproximación útil, no una verdad. Si tu producto es importante, prueba en un dispositivo real de gama media con depuración remota; es la única medida que no discute nadie.

Y la consecuencia metodológica: todas las cifras de este módulo están medidas en una máquina concreta —un portátil de desarrollo con estrangulamiento de CPU 4× y red Slow 4G, Chrome en modo incógnito— y sirven para ilustrar proporciones y órdenes de magnitud, no como valores universales. En tu equipo saldrán otros números. Lo que no cambiará es la conclusión: 600 llamadas a pintarTarjeta cuestan tres órdenes de magnitud más que un sort.

  1. La línea base de Nómada Tareas con 600 tareas

Ya tenemos método y herramientas. Vamos a establecer el número de partida de todo el módulo.

Primero, los datos. Necesitamos un tablero de 600 tareas realista y reproducible: si cada ejecución genera datos distintos, las mediciones no son comparables. Un generador con semilla fija lo resuelve.

// test/apoyo/backlog-grande.js
import { Tarea } from '../../js/modelo/tarea.js';

const RESPONSABLES = ['Iván', 'Lucía', 'Marta'];
const PRIORIDADES = ['alta', 'media', 'baja'];
const ESTADOS = ['pendiente', 'en-curso', 'hecha'];
const ETIQUETAS = ['espacio', 'serigrafía', 'web', 'carpintería', 'encuadernación', 'compras'];

/** Generador congruencial lineal: pseudoaleatorio pero REPRODUCIBLE. */
function aleatorioConSemilla(semilla) {
  let s = semilla;
  return () => {
    s = (s * 1664525 + 1013904223) % 4294967296;
    return s / 4294967296;
  };
}

/**
 * Dos años de Taller Nómada: 600 tareas con la misma forma que el backlog
 * canónico de 6, para que el modelo no note la diferencia.
 */
export function crearBacklogGrande(n = 600, semilla = 20260920) {
  const r = aleatorioConSemilla(semilla);
  const tareas = [];

  for (let i = 1; i <= n; i += 1) {
    const dia = 1 + Math.floor(r() * 27);
    const mes = 1 + Math.floor(r() * 12);
    tareas.push(new Tarea({
      id: i,
      titulo: `Tarea ${i} · ${ETIQUETAS[Math.floor(r() * ETIQUETAS.length)]}`,
      responsable: RESPONSABLES[Math.floor(r() * 3)],
      prioridad: PRIORIDADES[Math.floor(r() * 3)],
      estado: ESTADOS[Math.floor(r() * 3)],
      etiquetas: [ETIQUETAS[Math.floor(r() * ETIQUETAS.length)]],
      horasEstimadas: 1 + Math.floor(r() * 16),
      fechaLimite: `2026-${String(mes).padStart(2, '0')}-${String(dia).padStart(2, '0')}`,
      revisor: r() > 0.5 ? RESPONSABLES[Math.floor(r() * 3)] : null
    }));
  }
  return tareas;
}

Con la misma semilla, siempre las mismas 600 tareas. Eso es lo que permite comparar la medición de hoy con la de dentro de cuatro lecciones.

Y esta es la línea base, medida con el procedimiento del apartado 10 (mediana de 15 repeticiones tras 5 de calentamiento, CPU 4×, Slow 4G, incógnito, en el portátil de referencia):

# Medida Cómo se obtiene Línea base Objetivo Lección que la ataca
1 LCP (móvil simulado) Lighthouse móvil 4,1 s ≤ 2,5 s 09-05
2 INP al escribir en el buscador Performance, Interactions 480 ms ≤ 200 ms 09-02, 09-04
3 CLS Lighthouse móvil 0,21 ≤ 0,1 09-05
4 Tarea larga máxima en el arranque Performance, Main 1.180 ms ≤ 200 ms 09-02
5 render() completo, 600 tareas User Timing 310 ms ≤ 50 ms 09-04
6 Recálculos de resumen() por filtrado User Timing 96 ms ≤ 5 ms 09-02
7 Informe de planificación (bloquea el hilo) User Timing 940 ms 0 ms en el hilo principal 09-02
8 Memoria retenida tras 200 filtrados Memory, 3 instantáneas +37,7 MB ≈ 0 09-03
9 Nodos DOM del documento Performance, contador 7.812 ≤ 1.500 09-04
10 JS descargado antes de la 1.ª tarjeta Network 214 kB / 28 peticiones ≤ 80 kB / ≤ 5 09-05

Diez números. Ninguna opinión. Esta tabla es el contrato del módulo: la última lección la repetirá con la columna «después».

Fíjate en que la tabla también contiene un diagnóstico implícito: hay cuatro problemas distintos —código que tarda, memoria que se retiene, DOM que se maltrata y descarga excesiva— y cada uno tiene su lección. Eso no se sabía antes de medir; era perfectamente posible que todo fuera un único problema de red.

  1. El presupuesto de rendimiento y la integración continua

Una medición puntual se degrada. Dentro de tres meses alguien añadirá una librería de gráficos de 90 kB y nadie se dará cuenta hasta que un usuario se queje. Un presupuesto de rendimiento es un conjunto de umbrales que, al superarse, rompen la compilación: exactamente igual que una prueba de Jest que falla en 08-03.

Hay tres tipos de presupuesto y conviene tener de los tres:

Tipo Ejemplo Herramienta
De cantidad ≤ 80 kB de JS inicial; ≤ 5 peticiones Empaquetador (09-05)
De tiempo LCP ≤ 2,5 s; TBT ≤ 200 ms Lighthouse CI
De regla Todas las imágenes con width/height Auditoría de Lighthouse

Lighthouse CI hace lo de en medio con muy poca configuración:

npm install --save-dev @lhci/cli
{
  "ci": {
    "collect": {
      "url": ["http://localhost:4173/"],
      "numberOfRuns": 3,
      "startServerCommand": "npm run preview"
    },
    "assert": {
      "assertions": {
        "largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
        "total-blocking-time":      ["error", { "maxNumericValue": 200 }],
        "cumulative-layout-shift":  ["error", { "maxNumericValue": 0.1 }],
        "first-contentful-paint":   ["warn",  { "maxNumericValue": 1800 }],
        "uses-responsive-images":   "off"
      }
    },
    "upload": { "target": "temporary-public-storage" }
  }
}

Tres decisiones de ese fichero que no son cosméticas:

  • numberOfRuns: 3. Aplica la regla de repetir; Lighthouse CI se queda con la mediana.
  • error frente a warn. Solo lo que estás dispuesto a bloquear va como error. Un presupuesto que rompe la compilación todos los días acaba desactivado.
  • "uses-responsive-images": "off". Desactivar explícitamente lo que no aplica es mejor que ignorarlo: deja constancia de la decisión.

Y el paso en el flujo de trabajo de GitHub Actions, siguiendo el mismo orden barato-primero de 08-06:

# .github/workflows/ci.yml (fragmento)
  rendimiento:
    needs: [pruebas]              # lo barato primero: unitarias, después esto
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22, cache: npm }
      - run: npm ci
      - run: npm run build
      - run: npx lhci autorun     # devuelve código 1 si se incumple el presupuesto

Dos advertencias importantes sobre medir rendimiento en CI. La primera: los ejecutores compartidos son ruidosos; sus tiempos varían mucho más que tu máquina, así que pon los umbrales con margen y desconfía de una regresión de un 5 %. La segunda: los presupuestos de cantidad (kilobytes, número de peticiones, número de módulos) son mucho más estables que los de tiempo, y por eso son la primera línea de defensa. Los verás aplicados en 09-05.

Errores Comunes y Consejos

  • Optimizar sin medir. Es el error madre. Produce código más complicado, a veces más lento, y siempre más difícil de mantener.
  • Medir una sola vez. Una medición es una anécdota. Mínimo siete repeticiones, y mediana.
  • Usar la media en lugar de la mediana. Una sola pausa del recolector de basura la dispara y te lleva a una conclusión falsa.
  • Medir sin calentamiento. La primera ejecución no está optimizada por el JIT y puede ser diez veces más lenta.
  • Medir con extensiones activas. Inyectan scripts y observadores. Mide siempre en incógnito.
  • Medir con la caché caliente. Sin Disable cache estás midiendo tu segunda visita, no la primera de tu usuario.
  • Medir solo en tu portátil. Estrangula la CPU al menos 4×, o estarás optimizando para ti.
  • Perseguir el 100 de Lighthouse. La puntuación es una media ponderada comprimida; los últimos puntos cuestan muchísimo y no aportan nada perceptible.
  • Confundir laboratorio con campo. Si tu Lighthouse dice 1,9 s y Search Console dice 3,8 s, ninguno miente: uno es tu máquina y el otro el percentil 75 de la realidad.
  • Optimizar el TTFB tocando JavaScript. TTFB es del servidor. Diagnostica antes de actuar.
  • Cambiar cinco cosas a la vez. Si mejora, no sabrás cuál sirvió; si empeora, tampoco.
  • Dejar los console.log de instrumentación en producción. Cuestan, y dentro de un bucle cuestan muchísimo.
  • Consejo: guarda las grabaciones. El panel Performance permite exportar la traza como .json. Guarda la del «antes»: es la única forma de comparar de verdad.
  • Consejo: mira siempre el self time en Bottom-Up. El total time señala al que llama; el self time señala al culpable.
  • Consejo: escribe el objetivo antes de medir. «Que el filtrado responda en menos de 200 ms» es una meta; «que vaya más rápido» no.
  • Consejo: si una optimización no mejora la medición, revierte. Complejidad sin beneficio es deuda pura.

Ejercicios

Ejercicio 1 — El diagnóstico de tres frases. Marta dice «va lenta», Iván «al escribir se atasca» y Lucía «tarda en cargar». Para cada frase, indica: (a) qué Web Vital la captura, (b) qué herramienta usarías para confirmarla, (c) qué ajuste de estrangulamiento aplicarías y (d) qué medida concreta de la tabla de línea base corresponde. Después explica por qué las tres frases no pueden arreglarse con el mismo cambio.

Ejercicio 2 — Un banco de pruebas honesto. Usando banco() del apartado 10, escribe un script que compare dos formas de calcular las horas abiertas de las 600 tareas: (a) tareas.filter(t => t.estaAbierta()).reduce((s, t) => s + t.horasEstimadas, 0) y (b) un único for con acumulador. Ejecuta con 5 de calentamiento y 15 repeticiones, y responde con honestidad: ¿hay diferencia significativa? Justifica la respuesta con los números de mediana y p95, y di qué versión pondrías en el código.

Ejercicio 3 — Presupuesto para el buscador. El INP al escribir en el buscador es de 480 ms. Escribe (a) el objetivo numérico y su justificación perceptiva, (b) el fragmento de instrumentación con performance.mark/measure que mediría exactamente el trayecto «pulsación de tecla → tablero repintado», y (c) una aserción de Lighthouse CI que rompa la compilación si la interactividad se degrada. Explica por qué la aserción no puede usar directamente INP.

Soluciones

Solución 1

Frase (a) Web Vital (b) Herramienta (c) Estrangulamiento (d) Medida
«Va lenta» (Marta) Ninguno concreto: es un síntoma agregado. Se descompone en LCP + INP Panel Performance, grabación de una sesión de uso CPU 4× Es la suma de las medidas 4, 5 y 9
«Al escribir se atasca» (Iván) INP Performance, carril Interactions; en laboratorio, TBT CPU 4× (la red no interviene) Medidas 2 y 6
«Tarda en cargar» (Lucía) LCP (con TTFB y FCP como diagnóstico) Lighthouse móvil + panel Network CPU 4× y red Slow 4G Medidas 1 y 10

No pueden arreglarse con el mismo cambio porque suceden en momentos distintos y tienen causas distintas: la de Lucía ocurre antes de que se ejecute una línea de tu código de render —es un problema de cuánto se descarga y en qué orden (09-05)—, mientras que la de Iván ocurre con todo ya cargado y es puro trabajo del hilo principal (09-02 y 09-04). Optimizar el paquete no arreglará el atasco al escribir, y trocear el render no hará que la primera carga sea más rápida. La de Marta es la percepción agregada de las otras dos y desaparecerá cuando desaparezcan ambas.

Solución 2

// banco/horas-abiertas.js
import { Tablero } from '../js/modelo/tablero.js';
import { crearBacklogGrande } from '../test/apoyo/backlog-grande.js';
import { comparar } from '../js/util/banco.js';

const tareas = crearBacklogGrande(600);

const conCadena = () =>
  tareas.filter((t) => t.estaAbierta()).reduce((s, t) => s + t.horasEstimadas, 0);

const conBucle = () => {
  let suma = 0;
  for (let i = 0; i < tareas.length; i += 1) {
    if (tareas[i].estaAbierta()) suma += tareas[i].horasEstimadas;
  }
  return suma;
};

console.assert(conCadena() === conBucle(), 'las dos versiones deben dar lo mismo');
comparar({ nombre: 'filter+reduce', fn: conCadena }, { nombre: 'for', fn: conBucle },
         { calentamiento: 5, repeticiones: 15 });

Resultado en el portátil de referencia (sin estrangulamiento, porque aquí comparamos código, no experiencia):

Variante Mediana (ms) Mín (ms) p95 (ms)
filter+reduce 0,061 0,052 0,094
for 0,038 0,031 0,071

No hay diferencia significativa a efectos prácticos. Sí, el for es un 1,6× más rápido en la mediana, pero la diferencia absoluta es de 0,023 ms: veinte microsegundos, frente a los 310 ms que cuesta el render de la misma pantalla. Es 13.000 veces menos que el problema real. Además, el rango de las mediciones (0,052–0,094 frente a 0,031–0,071) se solapa parcialmente, señal de que hay ruido del mismo orden que el efecto.

Yo pondría filter/reduce en el código: se lee mejor, expresa la intención y su coste es irrelevante. Este ejercicio es, precisamente, la demostración experimental de por qué el mito «for es más rápido que los métodos de array» no debe guiar decisiones de diseño. Volveremos a él en 09-02 con la tabla completa de mitos.

Solución 3

(a) Objetivo: INP ≤ 200 ms, y dentro de eso el trayecto tecla → repintado ≤ 100 ms. La justificación es el umbral perceptivo del apartado 2: por debajo de ~100 ms la respuesta se siente instantánea; 200 ms es el límite que la métrica INP considera «bueno» porque incluye también el retraso de entrada y el pintado, que no controlas del todo.

(b) Instrumentación. La clave es que la medida no puede acabar cuando termina render(): tiene que acabar cuando el navegador ha pintado. Eso se consigue con un requestAnimationFrame anidado (07-06): el primero se ejecuta antes del pintado, el segundo justo después.

// js/vista/controlador.js — instrumentación del buscador
import { inicio, fin } from '../util/medicion.js';

campoBusqueda.addEventListener('input', (evento) => {
  inicio('buscador');

  vista.actualizar({ filtros: { texto: evento.target.value } });

  // El pintado ocurre DESPUÉS del manejador; hay que esperar dos fotogramas
  requestAnimationFrame(() => {
    requestAnimationFrame(() => {
      const ms = fin('buscador');
      if (ms > 100) console.warn(`Buscador lento: ${ms.toFixed(0)} ms`);
    });
  });
});

(c) Aserción de CI:

"total-blocking-time": ["error", { "maxNumericValue": 200 }],
"max-potential-fid":   ["warn",  { "maxNumericValue": 130 }]

INP no se puede usar directamente porque es una métrica de campo: necesita que un usuario real interactúe, y en Lighthouse CI nadie interactúa. Lo que sí se puede vigilar es el TBT, que mide el tiempo total en que el hilo principal está bloqueado y que correlaciona bien con un INP malo: si no hay tareas largas, no hay forma de que una interacción tarde 480 ms. Para vigilar el INP de verdad hace falta el web-vitals del apartado 9 enviando datos desde producción, con alerta si el percentil 75 supera los 200 ms.

Conclusión

Has cambiado el punto de partida de todo el módulo. «Nómada Tareas va lenta» ya no es una frase: es una tabla de diez números medidos con un procedimiento repetible sobre un tablero de 600 tareas generado con semilla fija.

Sabes por qué la intuición falla y qué decía Knuth de verdad: no «no optimices», sino «no optimices lo que no has medido, porque las suposiciones intuitivas de los programadores fallan». Conoces los umbrales que definen «rápido» para una persona —100 ms para sentir respuesta, 1 s para no perder el hilo, 16,7 ms por fotograma a 60 Hz, con menos de 10 ms de JavaScript dentro— y sabes traducirlos a objetivos falsables.

Dominas los Web Vitals: LCP (2,5 s) como medida de cuándo aparece el contenido principal, INP (200 ms) como medida de todas las interacciones de la sesión —no solo la primera, como el antiguo FID—, CLS (0,1) como medida de estabilidad visual, y TTFB y FCP como instrumentos de diagnóstico que dicen dónde está el problema de LCP. Y entiendes por qué tu Lighthouse local y los datos de campo nunca coinciden: uno es tu máquina con caché caliente, el otro el percentil 75 de móviles reales en redes reales.

Tienes el instrumental completo: Lighthouse leído por métricas y no por puntuación, con el TBT como sustituto de laboratorio del INP; el panel Performance con su procedimiento fijo —incógnito, estrangular, grabar poco— y su lectura por capas, con las tareas largas marcadas en rojo y la distinción entre Call Tree (quién lo provocó) y Bottom-Up ordenado por self time (quién lo consume); el panel Network con Disable cache y Slow 4G; la User Timing API con performance.mark/measure para que tus propias fases aparezcan en la línea de tiempo; y PerformanceObserver con la librería web-vitals y sendBeacon para el dato de campo. Y sabes hacer un benchmark honesto: calentar, repetir, mediana y p95, comparar contra una referencia, cambiar una sola variable, y descartar como ruido toda diferencia menor que la variabilidad de la propia medición.

Sobre todo, tienes el contrato del módulo: 4,1 s de LCP, 480 ms de INP, 0,21 de CLS, una tarea larga de 1.180 ms, 310 ms de render, 96 ms de recálculos, 940 ms de informe bloqueante, 37,7 MB retenidos, 7.812 nodos y 214 kB en 28 peticiones. Con su presupuesto de rendimiento correspondiente y un lhci autorun que rompe la integración continua cuando alguien lo incumple, porque un presupuesto que nadie vigila deja de existir en tres meses.

La tabla también reparte el trabajo. Las filas 2, 4, 6 y 7 —INP, tarea larga, recálculos y el informe de 940 ms que congela la pantalla— son código tuyo que tarda demasiado en ejecutarse: algoritmos con el coste equivocado, cálculos repetidos que podrían hacerse una vez, manejadores que se disparan sesenta veces por segundo y un trabajo pesado que se obstina en ocupar el único hilo que pinta la interfaz. Ahí está la mayor parte del segundo perdido, y ahí empieza la siguiente lección: Optimización del Rendimiento de JavaScript, donde verás cómo trabaja el motor por dentro lo justo para no sabotearlo, cambiarás una búsqueda cuadrática por un índice con Map, pondrás una caché bien invalidada en Tablero, aprenderás cuándo toca debounce y cuándo throttle, trocearás el trabajo largo para dejar respirar al bucle de eventos y, por fin, sacarás el informe de planificación del hilo principal metiéndolo en un Web Worker. Todo ello con la misma disciplina: medición antes, medición después, y revertir lo que no mejore.

Curso de JavaScript: De Principiante a Avanzado

Módulo 1: Introducción a JavaScript

Módulo 2: Estructuras de Control

Módulo 3: Funciones

Módulo 4: Objetos y Arrays

Módulo 5: Objetos y Funciones Avanzadas

Módulo 6: El Modelo de Objetos del Documento (DOM)

Módulo 7: APIs del Navegador y Temas Avanzados

Módulo 8: Pruebas y Depuración

Módulo 9: Rendimiento y Optimización

Módulo 10: Frameworks y Librerías de JavaScript

Módulo 11: Proyecto Final

© Copyright 2026. Todos los derechos reservados