
He visto muchas aplicaciones web que ofrecen una mala experiencia por **long tasks** que bloquean el hilo principal. Una interacción que tarda muchos milisegundos en responder, un scroll con muy poca fluidez, una interfaz que parece congelada. El problema suele ser el mismo: código JavaScript que no cede el control al navegador.

El **Interaction to Next Paint (INP)** es ahora una Core Web Vital crítica, y las long tasks (tareas que duran más de 50ms) lo degradan directamente. La solución está en una técnica llamada **"yield to main"**: dividir el trabajo en chunks y permitir que el navegador renderice y responda a las interacciones entre ellos.

En este artículo vamos a comparar las tres formas de hacer yield en JavaScript: `setTimeout`, `queueMicrotask` y `scheduler.postTask`. Con ejemplos medibles, casos reales y un árbol de decisión para saber cuál usar según nuestro caso.

## Tabla de Contenidos

- [El problema: Long Tasks y su impacto en INP](#el-problema-long-tasks-y-su-impacto-en-inp)
- [Fundamentos: El Event Loop](#fundamentos-el-event-loop)
- [Estrategia 1: setTimeout(fn, 0)](#estrategia-1-settimeoutfn-0)
- [Estrategia 2: queueMicrotask()](#estrategia-2-queuemicrotask)
- [Estrategia 3: scheduler.postTask()](#estrategia-3-schedulerposttask)
- [Comparación práctica](#comparación-práctica)
- [Ejemplos del mundo real](#ejemplos-del-mundo-real)
- [Medición y debugging](#medición-y-debugging)
- [Decision Tree: ¿Qué técnica usar?](#decision-tree-qué-técnica-usar)
- [Conclusiones](#conclusiones)
- [Próximos pasos](#próximos-pasos)

## El problema: Long Tasks y su impacto en INP

Cuando ejecutas código JavaScript que tarda más de 50ms, bloqueas el **main thread**. Durante ese tiempo:

- El navegador no puede responder a interacciones de usuarios y usuarias (clicks, taps, inputs)
- No puede actualizar la interfaz (rendering, animaciones)
- No puede procesar eventos pendientes

Esto impacta directamente en **INP** (Interaction to Next Paint), que mide el tiempo desde que alguien interactúa con nuestra web hasta que ve un feedback visual.

```javascript
// ❌ Bad: Long task que bloquea el main thread
function processLargeDataset(data) {
  const results = [];
  for (let i = 0; i < data.length; i++) {
    // Operación pesada que se ejecuta 10,000 veces
    results.push(heavyComputation(data[i]));
  }
  return results;
}

// Si data.length = 10,000 y cada iteración tarda 0.05ms
// → Long task de 500ms
// → INP degradado
// → Experiencia de usuaria/o terrible
```

**Medición real con Performance API:**

```javascript
const start = performance.now();
processLargeDataset(data);
const duration = performance.now() - start;
console.log(`Long task: ${duration}ms`); // → Long task: 487ms
```

## Fundamentos: El Event Loop

Antes de hablar de soluciones, necesitas entender cómo JavaScript decide qué ejecutar y cuándo.

### Anatomía del Event Loop

**Puntos clave:**

1. **Call Stack**: Código síncrono que se ejecuta inmediatamente
2. **Microtask Queue**: Promesas, `queueMicrotask()` - ejecutan antes del render
3. **Macrotask Queue**: `setTimeout`, eventos, I/O - ejecutan después del render
4. **Render Steps**: El navegador actualiza la UI cuando el call stack está vacío

### Ejemplo de orden de ejecución

```javascript
console.log("1: Síncrono");

setTimeout(() => {
  console.log("4: Macrotask (setTimeout)");
}, 0);

Promise.resolve().then(() => {
  console.log("3: Microtask (Promise)");
});

queueMicrotask(() => {
  console.log("3b: Microtask (queueMicrotask)");
});

console.log("2: Síncrono");

// Output:
// 1: Síncrono
// 2: Síncrono
// 3: Microtask (Promise)
// 3b: Microtask (queueMicrotask)
// 4: Macrotask (setTimeout)
```

**¿Por qué este orden?**

## Estrategia 1: setTimeout(fn, 0)

### Cómo funciona

`setTimeout` programa una **macrotask**: tu función se ejecuta después de que:

1. El call stack esté vacío
2. Todas las microtasks se hayan procesado
3. El navegador haya tenido oportunidad de renderizar

```javascript
function yieldWithSetTimeout() {
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}
```

### Ejemplo práctico con medición

```javascript
// ❌ Versión bloqueante
function processDataBlocking(items) {
  const results = [];
  const start = performance.now();

  for (let i = 0; i < items.length; i++) {
    results.push(processItem(items[i]));
  }

  const duration = performance.now() - start;
  console.log(`Long task: ${duration.toFixed(2)}ms`);
  // → Long task: 487.32ms

  return results;
}

// ✅ Versión con setTimeout (yield to main)
async function processDataWithYield(items) {
  const results = [];
  const CHUNK_SIZE = 100;
  const startTotal = performance.now();
  let taskCount = 0;

  for (let i = 0; i < items.length; i += CHUNK_SIZE) {
    const chunkStart = performance.now();

    // Procesar chunk
    const chunk = items.slice(i, i + CHUNK_SIZE);
    for (const item of chunk) {
      results.push(processItem(item));
    }

    const chunkDuration = performance.now() - chunkStart;
    console.log(`Chunk ${++taskCount}: ${chunkDuration.toFixed(2)}ms`);

    // Yield to main
    await new Promise(resolve => setTimeout(resolve, 0));
  }

  const totalDuration = performance.now() - startTotal;
  console.log(`Total: ${totalDuration.toFixed(2)}ms en ${taskCount} chunks`);
  // → Chunk 1: 12.45ms
  // → Chunk 2: 11.89ms
  // → ...
  // → Chunk 24: 12.12ms
  // → Total: 531.23ms en 24 chunks

  return results;
}
```

**Resultado:**

- **Antes**: 1 long task de 487ms
- **Después**: 24 tasks de ~12ms cada una
- **Trade-off**: +44ms de overhead total, pero INP mucho mejor

### Medición con Long Task API

```javascript
// Detectar long tasks automáticamente
if ("PerformanceObserver" in window) {
  const observer = new PerformanceObserver(list => {
    for (const entry of list.getEntries()) {
      console.warn(`⚠️ Long task detected: ${entry.duration.toFixed(2)}ms`);
      console.log("Attribution:", entry.attribution);
    }
  });

  observer.observe({ entryTypes: ["longtask"] });
}

// Versión bloqueante → ⚠️ Long task detected: 487.32ms
// Versión con yield → (sin warnings)
```

### Pros y contras

**✅ Pros:**

- Permite render entre chunks (mejora INP)
- Compatibilidad universal (todos los navegadores)
- Fácil de entender y debuggear
- No bloquea interacciones del usuario

**❌ Contras:**

- Overhead de ~1-4ms por cada `setTimeout`
- No controlas la prioridad de la tarea
- Delay mínimo de 4ms en navegadores modernos
- Puede ser demasiado lento para updates críticos de UI

### Caso de uso ideal

Usa `setTimeout(fn, 0)` cuando:

- Procesas datos grandes en background (parsing CSV, indexing, etc.)
- Renderizas listas largas progresivamente
- No necesitas control preciso de prioridad
- Requieres máxima compatibilidad con navegadores
- El trabajo puede esperar algunos milisegundos

```javascript
// Ejemplo: Renderizar lista infinita
async function renderLargeList(items, container) {
  const CHUNK_SIZE = 50;
  const fragment = document.createDocumentFragment();

  for (let i = 0; i < items.length; i += CHUNK_SIZE) {
    const chunk = items.slice(i, i + CHUNK_SIZE);

    // Renderizar chunk
    chunk.forEach(item => {
      const element = createListItem(item);
      fragment.appendChild(element);
    });

    container.appendChild(fragment);

    // Yield para permitir scroll, clicks, etc.
    await new Promise(r => setTimeout(r, 0));
  }
}
```

## Estrategia 2: queueMicrotask()

### Cómo funciona

`queueMicrotask` programa una **microtask**: la función se ejecuta:

1. Después del código síncrono actual
2. Antes de cualquier macrotask (`setTimeout`, eventos)
3. **Antes del render**

⚠️ `queueMicrotask` **NO hace yield to main** para rendering.

```javascript
function yieldWithMicrotask() {
  return new Promise(resolve => {
    queueMicrotask(resolve);
  });
}
```

### Diferencia con setTimeout en el Event Loop

**Clave:** Las microtasks se ejecutan **en batch** hasta vaciar la cola, ANTES del render.

### Ejemplo medible

```javascript
// Comparación directa: setTimeout vs queueMicrotask
async function compareYieldStrategies() {
  const ITERATIONS = 1000;

  // Test 1: setTimeout
  console.time("setTimeout yield");
  for (let i = 0; i < ITERATIONS; i++) {
    await new Promise(r => setTimeout(r, 0));
  }
  console.timeEnd("setTimeout yield");
  // → setTimeout yield: 4,237ms

  // Test 2: queueMicrotask
  console.time("queueMicrotask yield");
  for (let i = 0; i < ITERATIONS; i++) {
    await new Promise(r => queueMicrotask(r));
  }
  console.timeEnd("queueMicrotask yield");
  // → queueMicrotask yield: 18ms
}
```

**Resultado:** `queueMicrotask` es ~235x más rápido que `setTimeout`.

Pero... **¿permite render?**

```javascript
// Test: ¿Se actualiza el DOM entre chunks?
async function testRenderOpportunity(useSetTimeout = true) {
  let counter = document.getElementById("counter");
  if (!counter) {
    counter = document.createElement("div");
    counter.id = "counter";
    counter.style.cssText =
      "position:fixed;top:10px;left:10px;z-index:9999;padding:10px;background:#333;color:#fff;font-size:20px;font-family:monospace;";
    document.body.appendChild(counter);
  }
  const CHUNKS = 50;

  for (let i = 0; i < CHUNKS; i++) {
    counter.textContent = i;

    if (useSetTimeout) {
      await new Promise(r => setTimeout(r, 0));
    } else {
      await new Promise(r => queueMicrotask(r));
    }
  }
}

// setTimeout → Ves el contador incrementar visualmente
// queueMicrotask → Solo ves el valor final (49)
```

### Cuándo NO usar (trampas comunes)

❌ **NO uses `queueMicrotask` para yield to main**

```javascript
// ❌ Esto NO mejora INP
async function processDataWrong(items) {
  for (let i = 0; i < items.length; i += 100) {
    processChunk(items.slice(i, i + 100));
    await new Promise(r => queueMicrotask(r)); // ⚠️ No permite render
  }
}
```

❌ **NO crees loops infinitos de microtasks**

```javascript
// ❌ Esto congela el navegador completamente
function infiniteMicrotasks() {
  queueMicrotask(() => {
    console.log("Blocking...");
    infiniteMicrotasks(); // Recursión infinita
  });
}

// El navegador nunca puede renderizar
// Call stack nunca se vacía completamente
```

❌ **NO uses para operaciones pesadas**

```javascript
// ❌ Sigue siendo una long task
queueMicrotask(() => {
  // Esta operación tarda 200ms
  const result = heavyComputation();
  updateUI(result);
});

// La microtask bloquea el render hasta completarse
```

### Caso de uso ideal

Usa `queueMicrotask()` cuando:

- Necesitas ejecutar código después del frame actual pero ANTES del render
- Quieres asegurar consistencia de estado antes de pintar
- Necesitas máxima velocidad (sin overhead de `setTimeout`)
- **NO necesitas yield to main para INP**

```javascript
// ✅ Ejemplo válido: Batch de actualizaciones de estado
class StateManager {
  constructor() {
    this.state = {};
    this.pendingUpdates = [];
    this.isFlushScheduled = false;
  }

  setState(update) {
    this.pendingUpdates.push(update);

    if (!this.isFlushScheduled) {
      this.isFlushScheduled = true;
      queueMicrotask(() => this.flush());
    }
  }

  applyUpdate(update) {
    Object.assign(this.state, update);
  }

  flush() {
    // Aplicar todas las actualizaciones en batch
    this.pendingUpdates.forEach(update => this.applyUpdate(update));
    this.pendingUpdates = [];
    this.isFlushScheduled = false;

    // Ahora el render verá el estado consistente
  }
}

const stateManager = new StateManager();

// Múltiples setState() se batchean antes del render
stateManager.setState({ count: 1 });
stateManager.setState({ count: 2 });
stateManager.setState({ count: 3 });
// Solo 1 flush en la microtask queue
// Render ve directamente count: 3
```

## Estrategia 3: scheduler.postTask()

### API moderna con niveles de prioridad

`scheduler.postTask()` es la API moderna diseñada específicamente para scheduling con prioridades.

```javascript
if ("scheduler" in window && "postTask" in scheduler) {
  await scheduler.postTask(
    () => {
      // Código aquí
    },
    {
      priority: "user-visible", // user-blocking | user-visible | background
      delay: 0, // Opcional: delay en ms
      signal: abortController.signal, // Opcional: para cancelar
    }
  );
}
```

### Niveles de prioridad

### Ejemplo con diferentes prioridades

```javascript
async function demonstratePriorities() {
  console.log("Start");

  // Baja prioridad: analytics
  scheduler.postTask(() => console.log("4: Background - Analytics sent"), {
    priority: "background",
  });

  // Alta prioridad: respuesta a interacción
  scheduler.postTask(() => console.log("2: User-blocking - Button clicked"), {
    priority: "user-blocking",
  });

  // Prioridad media: actualizar UI
  scheduler.postTask(() => console.log("3: User-visible - UI updated"), {
    priority: "user-visible",
  });

  console.log("1: Synchronous");

  // Output:
  // Start
  // 1: Synchronous
  // 2: User-blocking - Button clicked
  // 3: User-visible - UI updated
  // 4: Background - Analytics sent
}
```

### Ejemplo práctico: Procesamiento con prioridades

```javascript
async function processDataWithPriorities(items) {
  const CHUNK_SIZE = 100;
  const results = [];

  // Chunk 1-3: Alta prioridad (contenido above-the-fold)
  for (let i = 0; i < Math.min(300, items.length); i += CHUNK_SIZE) {
    await scheduler.postTask(
      () => {
        const chunk = items.slice(i, i + CHUNK_SIZE);
        results.push(...chunk.map(processItem));
      },
      { priority: "user-blocking" }
    );
  }

  console.log("Contenido crítico renderizado");

  // Resto: Prioridad media
  for (let i = 300; i < items.length; i += CHUNK_SIZE) {
    await scheduler.postTask(
      () => {
        const chunk = items.slice(i, i + CHUNK_SIZE);
        results.push(...chunk.map(processItem));
      },
      { priority: "user-visible" }
    );
  }

  // Analytics: Baja prioridad
  scheduler.postTask(
    () => {
      sendAnalytics("data_processed", { count: items.length });
    },
    { priority: "background" }
  );

  return results;
}
```

### Cancelación de tareas

```javascript
async function cancellableTask() {
  const controller = new TaskController();

  const taskPromise = scheduler.postTask(
    async () => {
      for (let i = 0; i < 1000; i++) {
        await processItem(i);

        // Check si fue abortado
        if (controller.signal.aborted) {
          console.log("Task cancelled at iteration", i);
          return;
        }
      }
    },
    {
      priority: "background",
      signal: controller.signal,
    }
  );

  // Cancelar después de 100ms
  setTimeout(() => {
    controller.abort();
  }, 100);

  try {
    await taskPromise;
  } catch (error) {
    if (error.name === "AbortError") {
      console.log("Task was aborted");
    }
  }
}
```

### Polyfill / Fallback

```javascript
// Polyfill básico para navegadores sin soporte
if (!window.scheduler?.postTask) {
  window.scheduler = {
    postTask(callback, options = {}) {
      const { priority = "user-visible", delay = 0, signal } = options;

      return new Promise((resolve, reject) => {
        // Check si ya fue abortado
        if (signal?.aborted) {
          reject(new DOMException("Aborted", "AbortError"));
          return;
        }

        // Map priority to setTimeout delay
        const priorityDelays = {
          "user-blocking": Math.max(0, delay),
          "user-visible": Math.max(1, delay),
          background: Math.max(10, delay),
        };

        const actualDelay = priorityDelays[priority] || delay;

        const timeoutId = setTimeout(() => {
          try {
            const result = callback();
            resolve(result);
          } catch (error) {
            reject(error);
          }
        }, actualDelay);

        // Handle abort
        signal?.addEventListener("abort", () => {
          clearTimeout(timeoutId);
          reject(new DOMException("Aborted", "AbortError"));
        });
      });
    },
  };

  window.TaskController = class TaskController {
    constructor() {
      this.signal = {
        aborted: false,
        listeners: [],
        addEventListener(event, fn) {
          if (event === "abort") {
            this.listeners.push(fn);
          }
        },
      };
    }

    abort() {
      if (this.signal.aborted) return;
      this.signal.aborted = true;
      this.signal.listeners.forEach(fn => fn());
    }
  };
}
```

### Compatibilidad

```javascript
// Feature detection
function getSchedulerSupport() {
  return {
    postTask: "scheduler" in window && "postTask" in scheduler,
    priorities: "scheduler" in window && "postTask" in scheduler,
    cancellation: typeof TaskController !== "undefined",
  };
}

const support = getSchedulerSupport();
console.log("scheduler.postTask support:", support);

// Fallback progresivo
async function yieldToMain() {
  if (window.scheduler?.postTask) {
    return scheduler.postTask(() => {}, { priority: "user-visible" });
  }

  // Fallback a setTimeout
  return new Promise(resolve => setTimeout(resolve, 0));
}
```

**Estado actual (2026):**

- Chrome/Edge: ✅ Soportado desde v94
- Firefox: ⚠️ En desarrollo
- Safari: ❌ No soportado

### Caso de uso ideal

Usa `scheduler.postTask()` cuando:

- Necesitas control preciso de prioridades
- Desarrollo de aplicaciones modernas (Chrome/Edge)
- Quieres cancelar tareas programadas
- Puedes usar polyfill para fallback
- El delay es aceptable (~1-10ms según prioridad)

```javascript
// Ejemplo: Progressive rendering con prioridades
async function renderDashboard(data) {
  // 1. Crítico: Header y navigation
  await scheduler.postTask(() => renderHeader(data.user), {
    priority: "user-blocking",
  });

  // 2. Importante: Main content
  await scheduler.postTask(() => renderMainContent(data.metrics), {
    priority: "user-visible",
  });

  // 3. Secondary: Sidebar
  await scheduler.postTask(() => renderSidebar(data.widgets), {
    priority: "user-visible",
  });

  // 4. Bajo: Analytics y prefetch
  scheduler.postTask(
    () => {
      trackPageView();
      prefetchRelatedData();
    },
    { priority: "background" }
  );
}
```

## Comparación práctica

### Tabla comparativa

<div style="overflow-x: auto; margin: 2rem 0;">

| Característica            | `setTimeout(fn, 0)`     | `queueMicrotask(fn)` | `scheduler.postTask(fn)`   |
| ------------------------- | ----------------------- | -------------------- | -------------------------- |
| **Tipo de cola**          | Macrotask Queue         | Microtask Queue      | Task Queue (prioritizada)  |
| **Cuándo ejecuta**        | Después de render       | Antes de render      | Configurable por prioridad |
| **Permite yield to main** | ✅ Sí                   | ❌ No                | ✅ Sí                      |
| **Mejora INP**            | ✅✅✅ Excelente        | ❌ No aplica         | ✅✅✅ Excelente           |
| **Overhead**              | ~1-4ms por call         | <0.1ms por call      | ~1-5ms por call            |
| **Control de prioridad**  | ❌ No                   | ❌ No                | ✅✅✅ 3 niveles           |
| **Cancelable**            | ✅ Con `clearTimeout()` | ❌ No                | ✅ Con `AbortSignal`       |
| **Compatibilidad**        | ✅✅✅ Universal        | ✅✅ Moderna (IE11+) | ⚠️ Chrome/Edge only        |
| **Delay mínimo**          | ~4ms (spec)             | 0ms (inmediato)      | 0-10ms (según prioridad)   |
| **Uso de batería**        | Medio                   | Bajo                 | Medio-Alto                 |
| **Ideal para**            | Chunking general        | State batching       | Progressive rendering      |

</div>

### Benchmark: Las tres técnicas

```javascript
// Benchmark completo comparando las 3 estrategias
async function benchmarkYieldStrategies() {
  const DATA_SIZE = 10000;
  const CHUNK_SIZE = 100;
  const data = Array.from({ length: DATA_SIZE }, (_, i) => i);

  // Helper: simular trabajo pesado
  function heavyWork(item) {
    let result = item;
    for (let i = 0; i < 1000; i++) {
      result = Math.sqrt(result + i);
    }
    return result;
  }

  // Test 1: Sin yield (baseline)
  console.log("\n=== Test 1: Sin yield (bloqueante) ===");
  const t1Start = performance.now();
  const results1 = data.map(heavyWork);
  const t1Duration = performance.now() - t1Start;
  console.log(`Tiempo total: ${t1Duration.toFixed(2)}ms`);
  console.log(`Long tasks: 1 (${t1Duration.toFixed(2)}ms)`);

  // Test 2: setTimeout yield
  console.log("\n=== Test 2: setTimeout(fn, 0) ===");
  const t2Start = performance.now();
  const results2 = [];

  for (let i = 0; i < DATA_SIZE; i += CHUNK_SIZE) {
    const chunk = data.slice(i, i + CHUNK_SIZE);
    results2.push(...chunk.map(heavyWork));
    await new Promise(r => setTimeout(r, 0));
  }

  const t2Duration = performance.now() - t2Start;
  const chunks2 = Math.ceil(DATA_SIZE / CHUNK_SIZE);
  console.log(`Tiempo total: ${t2Duration.toFixed(2)}ms`);
  console.log(
    `Overhead: +${(t2Duration - t1Duration).toFixed(2)}ms (+${(((t2Duration - t1Duration) / t1Duration) * 100).toFixed(1)}%)`
  );
  console.log(
    `Tasks creadas: ${chunks2} (~${(t1Duration / chunks2).toFixed(2)}ms cada una)`
  );

  // Test 3: queueMicrotask yield
  console.log("\n=== Test 3: queueMicrotask(fn) ===");
  const t3Start = performance.now();
  const results3 = [];

  for (let i = 0; i < DATA_SIZE; i += CHUNK_SIZE) {
    const chunk = data.slice(i, i + CHUNK_SIZE);
    results3.push(...chunk.map(heavyWork));
    await new Promise(r => queueMicrotask(r));
  }

  const t3Duration = performance.now() - t3Start;
  const chunks3 = Math.ceil(DATA_SIZE / CHUNK_SIZE);
  console.log(`Tiempo total: ${t3Duration.toFixed(2)}ms`);
  console.log(
    `Overhead: +${(t3Duration - t1Duration).toFixed(2)}ms (+${(((t3Duration - t1Duration) / t1Duration) * 100).toFixed(1)}%)`
  );
  console.log(`⚠️ NO permite render entre chunks`);
  console.log(`Long task equivalente: ${t3Duration.toFixed(2)}ms`);

  // Test 4: scheduler.postTask yield
  let t4Duration;
  if (window.scheduler?.postTask) {
    console.log("\n=== Test 4: scheduler.postTask(fn) ===");
    const t4Start = performance.now();
    const results4 = [];

    for (let i = 0; i < DATA_SIZE; i += CHUNK_SIZE) {
      await scheduler.postTask(
        () => {
          const chunk = data.slice(i, i + CHUNK_SIZE);
          results4.push(...chunk.map(heavyWork));
        },
        { priority: "user-visible" }
      );
    }

    t4Duration = performance.now() - t4Start;
    const chunks4 = Math.ceil(DATA_SIZE / CHUNK_SIZE);
    console.log(`Tiempo total: ${t4Duration.toFixed(2)}ms`);
    console.log(
      `Overhead: +${(t4Duration - t1Duration).toFixed(2)}ms (+${(((t4Duration - t1Duration) / t1Duration) * 100).toFixed(1)}%)`
    );
    console.log(
      `Tasks creadas: ${chunks4} (~${(t1Duration / chunks4).toFixed(2)}ms cada una)`
    );
  }

  // Resumen
  console.log("\n=== Resumen ===");
  console.log(`Bloqueante: ${t1Duration.toFixed(2)}ms (baseline)`);
  console.log(
    `setTimeout: ${t2Duration.toFixed(2)}ms (+${(((t2Duration - t1Duration) / t1Duration) * 100).toFixed(1)}%)`
  );
  console.log(
    `queueMicrotask: ${t3Duration.toFixed(2)}ms (+${(((t3Duration - t1Duration) / t1Duration) * 100).toFixed(1)}%) ⚠️ NO yield`
  );
  if (window.scheduler?.postTask) {
    console.log(
      `scheduler.postTask: ${t4Duration.toFixed(2)}ms (+${(((t4Duration - t1Duration) / t1Duration) * 100).toFixed(1)}%)`
    );
  }
}

// Ejecutar benchmark
benchmarkYieldStrategies();

/* Output esperado:
=== Test 1: Sin yield (bloqueante) ===
Tiempo total: 487.32ms
Long tasks: 1 (487.32ms)

=== Test 2: setTimeout(fn, 0) ===
Tiempo total: 531.45ms
Overhead: +44.13ms (+9.1%)
Tasks creadas: 100 (~4.87ms cada una)

=== Test 3: queueMicrotask(fn) ===
Tiempo total: 489.21ms
Overhead: +1.89ms (+0.4%)
⚠️ NO permite render entre chunks
Long task equivalente: 489.21ms

=== Test 4: scheduler.postTask(fn) ===
Tiempo total: 528.67ms
Overhead: +41.35ms (+8.5%)
Tasks creadas: 100 (~4.87ms cada una)

=== Resumen ===
Bloqueante: 487.32ms (baseline)
setTimeout: 531.45ms (+9.1%)
queueMicrotask: 489.21ms (+0.4%) ⚠️ NO yield
scheduler.postTask: 528.67ms (+8.5%)
*/
```

### Impacto en INP

```javascript
// Medir INP real con las 3 estrategias
async function measureINPImpact(strategy = "setTimeout") {
  const DATA_SIZE = 5000;
  const CHUNK_SIZE = 100;
  const data = Array.from({ length: DATA_SIZE }, (_, i) => i);

  function heavyWork(item) {
    let result = item;
    for (let i = 0; i < 1000; i++) {
      result = Math.sqrt(result + i);
    }
    return result;
  }

  // Simular interacción durante el procesamiento
  let clickResponse = null;
  let button = document.getElementById("test-button");
  if (!button) {
    button = document.createElement("button");
    button.id = "test-button";
    button.textContent = "Click me during processing!";
    button.style.cssText =
      "position:fixed;top:10px;right:10px;z-index:9999;padding:10px;font-size:14px;cursor:pointer;";
    document.body.appendChild(button);
  }

  button.addEventListener("click", () => {
    const clickTime = performance.now();
    clickResponse = clickTime;
    button.textContent = "Clicked!";
  });

  console.log("👆 Click the button during processing...");

  const startTime = performance.now();

  // Procesar según estrategia
  switch (strategy) {
    case "blocking":
      data.forEach(item => heavyWork(item));
      break;

    case "setTimeout":
      for (let i = 0; i < DATA_SIZE; i += CHUNK_SIZE) {
        const chunk = data.slice(i, i + CHUNK_SIZE);
        chunk.forEach(heavyWork);
        await new Promise(r => setTimeout(r, 0));
      }
      break;

    case "queueMicrotask":
      for (let i = 0; i < DATA_SIZE; i += CHUNK_SIZE) {
        const chunk = data.slice(i, i + CHUNK_SIZE);
        chunk.forEach(heavyWork);
        await new Promise(r => queueMicrotask(r));
      }
      break;

    case "scheduler":
      for (let i = 0; i < DATA_SIZE; i += CHUNK_SIZE) {
        await scheduler.postTask(
          () => {
            const chunk = data.slice(i, i + CHUNK_SIZE);
            chunk.forEach(heavyWork);
          },
          { priority: "background" }
        );
      }
      break;
  }

  const endTime = performance.now();

  if (clickResponse) {
    // Calcular INP aproximado
    const inp = endTime - clickResponse;
    console.log(`\n📊 INP estimado: ${inp.toFixed(2)}ms`);
    console.log(`✅ Bueno: <200ms | ⚠️ Mejorar: 200-500ms | ❌ Malo: >500ms`);

    if (inp < 200) console.log("✅ INP excelente");
    else if (inp < 500) console.log("⚠️ INP necesita mejora");
    else console.log("❌ INP muy malo");
  }

  console.log(`\nProcesamiento total: ${(endTime - startTime).toFixed(2)}ms`);
}
```

## Ejemplos del mundo real

### Ejemplo 1: Procesar array grande

```javascript
/**
 * Caso real: Importar y procesar un CSV de 50MB con 100,000 registros
 * Objetivo: No bloquear la UI durante el procesamiento
 */

// Datos de ejemplo
const csvData = Array.from({ length: 100000 }, (_, i) => ({
  id: i,
  name: `Product ${i}`,
  price: Math.random() * 1000,
  stock: Math.floor(Math.random() * 100),
}));

// ❌ Versión bloqueante: 1 long task
function processCSVBlocking(data) {
  console.log("Processing CSV (blocking)...");
  const start = performance.now();

  const processed = data.map(row => ({
    ...row,
    priceWithTax: row.price * 1.21,
    inStock: row.stock > 0,
  }));

  const duration = performance.now() - start;
  console.log(`❌ Long task: ${duration.toFixed(2)}ms`);

  return processed;
}

// ✅ Versión 1: setTimeout (máxima compatibilidad)
async function processCSVSetTimeout(data) {
  console.log("Processing CSV (setTimeout)...");
  const start = performance.now();
  const CHUNK_SIZE = 1000;
  const processed = [];

  for (let i = 0; i < data.length; i += CHUNK_SIZE) {
    const chunk = data.slice(i, i + CHUNK_SIZE);

    const chunkProcessed = chunk.map(row => ({
      ...row,
      priceWithTax: row.price * 1.21,
      inStock: row.stock > 0,
    }));

    processed.push(...chunkProcessed);

    // Update progress
    const progress = Math.round((i / data.length) * 100);
    updateProgressBar(progress);

    // Yield to main
    await new Promise(r => setTimeout(r, 0));
  }

  const duration = performance.now() - start;
  console.log(
    `✅ Total: ${duration.toFixed(2)}ms en ${Math.ceil(data.length / CHUNK_SIZE)} chunks`
  );

  return processed;
}

// ✅ Versión 2: scheduler.postTask (con prioridades)
async function processCSVScheduler(data) {
  if (!window.scheduler?.postTask) {
    console.warn("scheduler.postTask no disponible, usando setTimeout");
    return processCSVSetTimeout(data);
  }

  console.log("Processing CSV (scheduler.postTask)...");
  const start = performance.now();
  const CHUNK_SIZE = 1000;
  const processed = [];

  // Primeros 10% con alta prioridad (preview)
  const previewSize = Math.floor(data.length * 0.1);

  for (let i = 0; i < previewSize; i += CHUNK_SIZE) {
    await scheduler.postTask(
      () => {
        const chunk = data.slice(i, i + CHUNK_SIZE);
        const chunkProcessed = chunk.map(row => ({
          ...row,
          priceWithTax: row.price * 1.21,
          inStock: row.stock > 0,
        }));
        processed.push(...chunkProcessed);
      },
      { priority: "user-visible" }
    );
  }

  console.log("✅ Preview renderizado");

  // Resto con prioridad background
  for (let i = previewSize; i < data.length; i += CHUNK_SIZE) {
    await scheduler.postTask(
      () => {
        const chunk = data.slice(i, i + CHUNK_SIZE);
        const chunkProcessed = chunk.map(row => ({
          ...row,
          priceWithTax: row.price * 1.21,
          inStock: row.stock > 0,
        }));
        processed.push(...chunkProcessed);

        const progress = Math.round((i / data.length) * 100);
        updateProgressBar(progress);
      },
      { priority: "background" }
    );
  }

  const duration = performance.now() - start;
  console.log(`✅ Total: ${duration.toFixed(2)}ms`);

  return processed;
}

// Helper: Update progress bar
function updateProgressBar(percent) {
  const bar = document.getElementById("progress-bar");
  if (bar) bar.style.width = `${percent}%`;
}

// Comparación con Long Task API
async function compareCSVProcessing() {
  const observer = new PerformanceObserver(list => {
    for (const entry of list.getEntries()) {
      console.warn(`⚠️ Long task: ${entry.duration.toFixed(2)}ms`);
    }
  });
  observer.observe({ entryTypes: ["longtask"] });

  console.log("\n=== Test 1: Blocking ===");
  processCSVBlocking(csvData);
  // → ⚠️ Long task: 487.32ms

  await new Promise(r => setTimeout(r, 100));

  console.log("\n=== Test 2: setTimeout ===");
  await processCSVSetTimeout(csvData);
  // → (sin warnings de long tasks)

  await new Promise(r => setTimeout(r, 100));

  console.log("\n=== Test 3: scheduler.postTask ===");
  await processCSVScheduler(csvData);
  // → (sin warnings de long tasks)
}
```

### Ejemplo 2: Async Generator Transformation

```javascript
/**
 * Caso real: Transformar generadores async para yield to main
 * Problema original: async/await transpilado crea long tasks
 */

// Función original: async function transpilada por Babel/TypeScript
function _asyncToGenerator(fn) {
  return function () {
    var self = this;
    var args = arguments;

    return new Promise(function (resolve, reject) {
      var gen = fn.apply(self, args);

      function _next(value) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
      }

      function _throw(err) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
      }

      _next(undefined);
    });
  };
}

function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
  try {
    var info = gen[key](arg);
    var value = info.value;
  } catch (error) {
    reject(error);
    return;
  }

  if (info.done) {
    resolve(value);
  } else {
    // ❌ Aquí está el problema: no hay yield to main
    Promise.resolve(value).then(_next, _throw);
  }
}

// ✅ Versión mejorada: Con yield to main usando setTimeout
function asyncGeneratorStepWithYield(
  gen,
  resolve,
  reject,
  _next,
  _throw,
  key,
  arg
) {
  try {
    var info = gen[key](arg);
    var value = info.value;
  } catch (error) {
    reject(error);
    return;
  }

  if (info.done) {
    resolve(value);
  } else {
    // ✅ Yield to main cada N iteraciones
    Promise.resolve(value).then(val => {
      // Yield cada 50ms de trabajo
      if (performance.now() - gen._lastYield > 50) {
        setTimeout(() => {
          gen._lastYield = performance.now();
          _next(val);
        }, 0);
      } else {
        _next(val);
      }
    }, _throw);
  }
}

// ✅ Versión con scheduler.postTask (control de prioridad)
function asyncGeneratorStepWithScheduler(
  gen,
  resolve,
  reject,
  _next,
  _throw,
  key,
  arg,
  priority = "user-visible"
) {
  try {
    var info = gen[key](arg);
    var value = info.value;
  } catch (error) {
    reject(error);
    return;
  }

  if (info.done) {
    resolve(value);
  } else {
    Promise.resolve(value).then(async val => {
      if (performance.now() - gen._lastYield > 50) {
        if (window.scheduler?.postTask) {
          await scheduler.postTask(
            () => {
              gen._lastYield = performance.now();
              _next(val);
            },
            { priority }
          );
        } else {
          setTimeout(() => {
            gen._lastYield = performance.now();
            _next(val);
          }, 0);
        }
      } else {
        _next(val);
      }
    }, _throw);
  }
}

// Ejemplo de uso
async function* heavyGenerator() {
  for (let i = 0; i < 10000; i++) {
    // Simulación de trabajo pesado
    const result = expensiveOperation(i);
    yield result;
  }
}

// Consumir con yield to main
async function consumeGenerator() {
  console.time("Generator with yield");

  const gen = heavyGenerator();
  gen._lastYield = performance.now();

  const results = [];
  for await (const value of gen) {
    results.push(value);
  }

  console.timeEnd("Generator with yield");
  console.log(`Processed ${results.length} items without long tasks`);
}
```

### Ejemplo 3: React Reconciliation (conceptual)

```javascript
/**
 * Cómo React usa scheduling para mantener la UI responsiva
 * (Simplificación conceptual de React Fiber + Scheduler)
 */

// React usa scheduler.postTask internamente (cuando disponible)
// con un sistema de prioridades similar

const ReactPriorities = {
  ImmediatePriority: 1, // user-blocking
  UserBlockingPriority: 2, // user-visible
  NormalPriority: 3, // user-visible
  LowPriority: 4, // background
  IdlePriority: 5, // background
};

// Workloop simplificado de React Fiber
class ReactScheduler {
  constructor() {
    this.workQueue = [];
    this.isWorking = false;
  }

  scheduleWork(work, priority) {
    this.workQueue.push({ work, priority });
    this.workQueue.sort((a, b) => a.priority - b.priority);

    if (!this.isWorking) {
      this.performWork();
    }
  }

  async performWork() {
    this.isWorking = true;
    let startTime = performance.now();

    while (this.workQueue.length > 0) {
      const { work, priority } = this.workQueue.shift();

      // Ejecutar un "unit of work"
      const deadline = this.getDeadlineForPriority(priority);
      const shouldYield = performance.now() - startTime > deadline;

      if (shouldYield) {
        // ✅ Yield to main
        await this.yieldToMain(priority);
        startTime = performance.now();
      }

      // Procesar el trabajo
      work();
    }

    this.isWorking = false;
  }

  getDeadlineForPriority(priority) {
    switch (priority) {
      case ReactPriorities.ImmediatePriority:
        return -1; // No yield, ejecutar inmediatamente
      case ReactPriorities.UserBlockingPriority:
        return 250; // Yield después de 250ms
      case ReactPriorities.NormalPriority:
        return 5000;
      case ReactPriorities.LowPriority:
        return 10000;
      case ReactPriorities.IdlePriority:
        return Infinity;
      default:
        return 5000;
    }
  }

  async yieldToMain(priority) {
    if (window.scheduler?.postTask) {
      const schedulerPriority = this.mapToSchedulerPriority(priority);
      await scheduler.postTask(() => {}, { priority: schedulerPriority });
    } else {
      // Fallback: setTimeout
      await new Promise(r => setTimeout(r, 0));
    }
  }

  mapToSchedulerPriority(reactPriority) {
    if (reactPriority <= ReactPriorities.UserBlockingPriority) {
      return "user-blocking";
    } else if (reactPriority === ReactPriorities.NormalPriority) {
      return "user-visible";
    } else {
      return "background";
    }
  }
}

// Ejemplo: Renderizar lista grande con React scheduling
const reactScheduler = new ReactScheduler();

function renderLargeList(items) {
  const container = document.getElementById("list");
  const CHUNK_SIZE = 50;

  for (let i = 0; i < items.length; i += CHUNK_SIZE) {
    const chunk = items.slice(i, i + CHUNK_SIZE);

    // Prioridad alta para el contenido visible
    const priority =
      i < 200
        ? ReactPriorities.UserBlockingPriority
        : ReactPriorities.NormalPriority;

    reactScheduler.scheduleWork(() => {
      const fragment = document.createDocumentFragment();
      chunk.forEach(item => {
        const element = createListItem(item);
        fragment.appendChild(element);
      });
      container.appendChild(fragment);
    }, priority);
  }
}

// Resultado: UI permanece responsiva durante el renderizado
```

## Medición y debugging

### DevTools Performance Panel

```javascript
/**
 * Snippet para copiar en DevTools Console
 * Mide el impacto de yield strategies en real-time
 */

(async function measureYieldImpact() {
  console.clear();
  console.log("🔧 Yield Strategy Measurement Tool\n");

  // Configuración
  const DATA_SIZE = 5000;
  const CHUNK_SIZE = 100;
  const data = Array.from({ length: DATA_SIZE }, (_, i) => i);

  // Trabajo pesado simulado
  function heavyWork(n) {
    let result = n;
    for (let i = 0; i < 10000; i++) {
      result = Math.sqrt(result + i);
    }
    return result;
  }

  // Observer para long tasks
  const longTasks = [];
  const observer = new PerformanceObserver(list => {
    for (const entry of list.getEntries()) {
      longTasks.push(entry.duration);
    }
  });
  observer.observe({ entryTypes: ["longtask"] });

  // Test runner
  async function test(name, yieldFn) {
    longTasks.length = 0;
    console.log(`\n📊 Testing: ${name}`);

    // Mark inicio
    performance.mark(`${name}-start`);

    for (let i = 0; i < DATA_SIZE; i += CHUNK_SIZE) {
      const chunk = data.slice(i, i + CHUNK_SIZE);
      chunk.forEach(heavyWork);

      if (yieldFn) {
        await yieldFn();
      }
    }

    // Mark fin
    performance.mark(`${name}-end`);
    performance.measure(name, `${name}-start`, `${name}-end`);

    // Resultados
    const measure = performance.getEntriesByName(name)[0];
    console.log(`  ⏱️  Total: ${measure.duration.toFixed(2)}ms`);
    console.log(`  📦 Chunks: ${Math.ceil(DATA_SIZE / CHUNK_SIZE)}`);
    console.log(`  ⚠️  Long tasks: ${longTasks.length}`);

    if (longTasks.length > 0) {
      const avgLongTask =
        longTasks.reduce((a, b) => a + b, 0) / longTasks.length;
      console.log(`  📏 Avg long task: ${avgLongTask.toFixed(2)}ms`);
    }

    // Limpiar marks
    performance.clearMarks(`${name}-start`);
    performance.clearMarks(`${name}-end`);
    performance.clearMeasures(name);

    return measure.duration;
  }

  // Ejecutar tests
  const results = {};

  // 1. Sin yield
  results.blocking = await test("Blocking (no yield)", null);
  await new Promise(r => setTimeout(r, 500));

  // 2. setTimeout
  results.setTimeout = await test("setTimeout", () =>
    new Promise(r => setTimeout(r, 0)));
  await new Promise(r => setTimeout(r, 500));

  // 3. queueMicrotask
  results.queueMicrotask = await test("queueMicrotask", () =>
    new Promise(r => queueMicrotask(r)));
  await new Promise(r => setTimeout(r, 500));

  // 4. scheduler.postTask (si disponible)
  if (window.scheduler?.postTask) {
    results.scheduler = await test("scheduler.postTask", () =>
      scheduler.postTask(() => {}, { priority: "user-visible" }));
  }

  // Resumen
  console.log("\n" + "=".repeat(50));
  console.log("📊 RESUMEN");
  console.log("=".repeat(50));

  Object.entries(results).forEach(([name, duration]) => {
    const overhead = duration - results.blocking;
    const overheadPercent = (overhead / results.blocking) * 100;
    console.log(
      `${name.padEnd(20)} ${duration.toFixed(2)}ms (+${overheadPercent.toFixed(1)}%)`
    );
  });

  observer.disconnect();
  console.log("\n✅ Medición completada");
})();
```

**Cómo usar:**

1. Abre DevTools
2. Ve a la pestaña "Console"
3. Copia y pega el snippet
4. Presiona Enter
5. Cambia a la pestaña "Performance" y observa el flamegraph

### Identificar long tasks visualmente

```javascript
// Agregar overlay visual para long tasks
class LongTaskVisualizer {
  constructor() {
    this.overlay = this.createOverlay();
    this.observer = new PerformanceObserver(list => {
      for (const entry of list.getEntries()) {
        this.showLongTask(entry);
      }
    });
    this.observer.observe({ entryTypes: ["longtask"] });
  }

  createOverlay() {
    const overlay = document.createElement("div");
    overlay.style.cssText = `
      position: fixed;
      top: 0;
      right: 0;
      padding: 10px;
      background: rgba(255, 0, 0, 0.8);
      color: white;
      font-family: monospace;
      font-size: 12px;
      z-index: 9999;
      display: none;
    `;
    document.body.appendChild(overlay);
    return overlay;
  }

  showLongTask(entry) {
    this.overlay.textContent = `⚠️ Long Task: ${entry.duration.toFixed(2)}ms`;
    this.overlay.style.display = "block";

    setTimeout(() => {
      this.overlay.style.display = "none";
    }, 2000);

    console.warn(`Long task detected:`, {
      duration: entry.duration,
      startTime: entry.startTime,
      attribution: entry.attribution,
    });
  }

  disconnect() {
    this.observer.disconnect();
    this.overlay.remove();
  }
}

// Activar visualizador
const visualizer = new LongTaskVisualizer();

// Para desactivar: visualizer.disconnect();
```

### Performance Marks y Measures

```javascript
// Sistema de medición detallado
class PerformanceTracker {
  constructor(name) {
    this.name = name;
    this.metrics = {
      chunks: [],
      yields: 0,
      longTasks: 0,
      totalDuration: 0,
    };
  }

  startChunk(chunkId) {
    performance.mark(`${this.name}-chunk-${chunkId}-start`);
  }

  endChunk(chunkId) {
    performance.mark(`${this.name}-chunk-${chunkId}-end`);
    performance.measure(
      `${this.name}-chunk-${chunkId}`,
      `${this.name}-chunk-${chunkId}-start`,
      `${this.name}-chunk-${chunkId}-end`
    );

    const measure = performance.getEntriesByName(
      `${this.name}-chunk-${chunkId}`
    )[0];
    this.metrics.chunks.push(measure.duration);

    if (measure.duration > 50) {
      this.metrics.longTasks++;
    }
  }

  recordYield() {
    this.metrics.yields++;
  }

  start() {
    performance.mark(`${this.name}-start`);
  }

  end() {
    performance.mark(`${this.name}-end`);
    performance.measure(this.name, `${this.name}-start`, `${this.name}-end`);

    const measure = performance.getEntriesByName(this.name)[0];
    this.metrics.totalDuration = measure.duration;
  }

  report() {
    const avgChunkDuration =
      this.metrics.chunks.reduce((a, b) => a + b, 0) /
      this.metrics.chunks.length;
    const maxChunkDuration = Math.max(...this.metrics.chunks);

    console.log(`\n📊 Performance Report: ${this.name}`);
    console.log("─".repeat(50));
    console.log(`Total duration: ${this.metrics.totalDuration.toFixed(2)}ms`);
    console.log(`Chunks: ${this.metrics.chunks.length}`);
    console.log(`Yields: ${this.metrics.yields}`);
    console.log(`Long tasks: ${this.metrics.longTasks}`);
    console.log(`Avg chunk: ${avgChunkDuration.toFixed(2)}ms`);
    console.log(`Max chunk: ${maxChunkDuration.toFixed(2)}ms`);
    console.log("─".repeat(50));

    return this.metrics;
  }

  clear() {
    // Limpiar todas las marks y measures
    this.metrics.chunks.forEach((_, i) => {
      performance.clearMarks(`${this.name}-chunk-${i}-start`);
      performance.clearMarks(`${this.name}-chunk-${i}-end`);
      performance.clearMeasures(`${this.name}-chunk-${i}`);
    });
    performance.clearMarks(`${this.name}-start`);
    performance.clearMarks(`${this.name}-end`);
    performance.clearMeasures(this.name);
  }
}

// Uso
async function processWithTracking(data, yieldStrategy) {
  const tracker = new PerformanceTracker(`Process-${yieldStrategy}`);
  tracker.start();

  const CHUNK_SIZE = 100;

  for (let i = 0; i < data.length; i += CHUNK_SIZE) {
    const chunkId = Math.floor(i / CHUNK_SIZE);
    tracker.startChunk(chunkId);

    const chunk = data.slice(i, i + CHUNK_SIZE);
    chunk.forEach(processItem);

    tracker.endChunk(chunkId);

    // Yield según estrategia
    switch (yieldStrategy) {
      case "setTimeout":
        await new Promise(r => setTimeout(r, 0));
        break;
      case "scheduler":
        await scheduler.postTask(() => {}, { priority: "user-visible" });
        break;
    }

    tracker.recordYield();
  }

  tracker.end();
  const report = tracker.report();
  tracker.clear();

  return report;
}
```

### Integration con Real User Monitoring (RUM)

```javascript
// Enviar métricas a tu sistema de RUM
class RUMReporter {
  constructor(endpoint) {
    this.endpoint = endpoint;
    this.metrics = [];

    // Observer para INP
    if ("PerformanceObserver" in window) {
      this.observeINP();
      this.observeLongTasks();
    }
  }

  observeINP() {
    const observer = new PerformanceObserver(list => {
      for (const entry of list.getEntries()) {
        if (entry.name === "first-input") {
          this.metrics.push({
            type: "inp",
            value: entry.processingStart - entry.startTime,
            timestamp: Date.now(),
          });
        }
      }
    });

    observer.observe({
      type: "event",
      buffered: true,
      durationThreshold: 0,
    });
  }

  observeLongTasks() {
    const observer = new PerformanceObserver(list => {
      for (const entry of list.getEntries()) {
        this.metrics.push({
          type: "longtask",
          duration: entry.duration,
          attribution: entry.attribution?.[0]?.name || "unknown",
          timestamp: Date.now(),
        });
      }
    });

    observer.observe({ entryTypes: ["longtask"] });
  }

  async flush() {
    if (this.metrics.length === 0) return;

    const payload = {
      url: window.location.href,
      userAgent: navigator.userAgent,
      metrics: this.metrics,
    };

    try {
      await fetch(this.endpoint, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(payload),
        keepalive: true, // Important para enviar antes de unload
      });

      this.metrics = [];
    } catch (error) {
      console.error("RUM reporting failed:", error);
    }
  }
}

// Inicializar
const rum = new RUMReporter("https://your-rum-endpoint.com/metrics");

// Flush al salir de la página
window.addEventListener("visibilitychange", () => {
  if (document.visibilityState === "hidden") {
    rum.flush();
  }
});
```

## Decision Tree: ¿Qué técnica usar?

### Preguntas clave para decidir

1. **¿Necesitas yield to main para mejorar INP?**
   - **SÍ** → Descarta `queueMicrotask`, considera `setTimeout` o `scheduler.postTask`
   - **NO** → `queueMicrotask` es válido para batching de estado

2. **¿Necesitas control de prioridad?**
   - **SÍ** → `scheduler.postTask` (con fallback a `setTimeout`)
   - **NO** → `setTimeout(fn, 0)` es suficiente

3. **¿Qué tan crítico es el soporte de navegadores?**
   - **Universal** → `setTimeout` (funciona en todos)
   - **Moderno** → `scheduler.postTask` con polyfill

4. **¿Cuánto dura la tarea?**
   - **<50ms** → No necesitas yield, ejecuta directamente
   - **50-200ms** → Divide en 2-4 chunks con `setTimeout`
   - **>200ms** → Usa `scheduler.postTask` con prioridades

5. **¿La tarea bloquea interacciones de usuarios y usuarias?**
   - **SÍ** → Usa `user-blocking` priority
   - **NO** → Usa `user-visible` o `background` priority

### Tabla de decisión rápida

| Caso de uso                     | Técnica recomendada                  | Alternativa                          |
| ------------------------------- | ------------------------------------ | ------------------------------------ |
| **Procesar array grande**       | `setTimeout` + chunking              | `scheduler.postTask` (background)    |
| **Renderizar lista progresiva** | `scheduler.postTask` (user-visible)  | `setTimeout` + chunking              |
| **Responder a click**           | `scheduler.postTask` (user-blocking) | Código síncrono si <50ms             |
| **Batch de setState**           | `queueMicrotask`                     | `Promise.resolve().then()`           |
| **Import de CSV**               | `setTimeout` + progress              | `scheduler.postTask` (background)    |
| **Animación frame-by-frame**    | `requestAnimationFrame`              | N/A (no yield)                       |
| **Analytics / tracking**        | `scheduler.postTask` (background)    | `setTimeout` con delay               |
| **Preload de recursos**         | `requestIdleCallback`                | `scheduler.postTask` (background)    |
| **Validación de formulario**    | Código síncrono                      | `scheduler.postTask` (user-blocking) |
| **Search indexing**             | `scheduler.postTask` (background)    | `requestIdleCallback`                |

## Conclusiones

Después de analizar las tres estrategias, estas son las lecciones clave:

### Resumen de cuándo usar cada técnica

**`setTimeout(fn, 0)` - El clásico confiable**

- ✅ Usa cuando necesites máxima compatibilidad
- ✅ Ideal para chunking general de tareas largas
- ✅ Buena opción si no necesitas control de prioridad
- ⚠️ ~4ms de overhead por yield
- ⚠️ No controlas la prioridad de ejecución

**`queueMicrotask(fn)` - El rápido pero peligroso**

- ✅ Perfecto para batching de actualizaciones de estado
- ✅ Garantiza consistencia antes del render
- ✅ Overhead mínimo (<0.1ms)
- ❌ **NO usa para yield to main** (no mejora INP)
- ❌ Puede bloquear el render si creas muchas microtasks

**`scheduler.postTask(fn)` - El moderno y poderoso**

- ✅ Control preciso de prioridades (user-blocking, user-visible, background)
- ✅ Cancelación con AbortSignal
- ✅ API diseñada específicamente para scheduling
- ⚠️ Solo Chrome/Edge (necesita polyfill)
- ⚠️ ~1-5ms de overhead según prioridad

### Recomendaciones para optimizar INP

1. **Mide primero, optimiza después**
   - Usa Long Task API para detectar problemas
   - Performance panel en DevTools es vuestro mejor amigo
   - Establece budget: ninguna tarea >50ms

2. **Divide tareas grandes en chunks de 16-50ms**
   - 16ms = 1 frame a 60fps
   - 50ms = threshold de long task
   - Usa `yieldToMain()` entre chunks

3. **Prioriza el trabajo crítico**
   - First paint: `user-blocking`
   - Contenido visible: `user-visible`
   - Analytics/tracking: `background`

4. **No abuses de microtasks**
   - `queueMicrotask` no mejora INP
   - Úsalo solo para consistency de estado
   - Cuidado con loops infinitos

5. **Ten un plan de fallback**
   - Polyfill para `scheduler.postTask`
   - Feature detection siempre
   - `setTimeout` como red de seguridad

### Implementación práctica

```javascript
// Helper universal para yield to main
async function yieldToMain(priority = "user-visible") {
  // Preferencia: scheduler.postTask
  if (window.scheduler?.postTask) {
    return scheduler.postTask(() => {}, { priority });
  }

  // Fallback: setTimeout
  return new Promise(resolve => setTimeout(resolve, 0));
}

// Usar en vuestro código
async function processLargeTask(data) {
  const CHUNK_SIZE = 100;
  const results = [];

  for (let i = 0; i < data.length; i += CHUNK_SIZE) {
    // Procesar chunk
    const chunk = data.slice(i, i + CHUNK_SIZE);
    results.push(...chunk.map(processItem));

    // Yield to main
    await yieldToMain("user-visible");
  }

  return results;
}
```

### Recursos adicionales

**Documentación oficial:**

- [MDN - setTimeout](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout)
- [MDN - queueMicrotask](https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask)
- [WICG - Scheduler API](https://wicg.github.io/scheduling-apis/)

**Web Performance:**

- [web.dev - Optimize INP](https://web.dev/articles/optimize-inp)
- [web.dev - Long Tasks](https://web.dev/articles/long-tasks-devtools)
- [Chrome DevTools - Performance](https://developer.chrome.com/docs/devtools/performance/)

**Event Loop:**

- [HTML Living Standard - Event Loop](https://html.spec.whatwg.org/multipage/webappapis.html#event-loops)
- [Jake Archibald - Tasks, microtasks, queues and schedules](https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/)

**Visualizadores:**

- [Loupe - Event Loop Visualizer](http://latentflip.com/loupe/)
- [JavaScript Visualizer](https://www.jsv9000.app/)

## Próximos pasos

Para mejorar el INP de vuestra aplicación:

1. **Audita el código actual**
   - Ejecuta el snippet de DevTools incluido
   - Identifica long tasks con Performance Observer
   - Mide INP real con Web Vitals

2. **Implementa yielding progresivamente**
   - Empieza por las tareas más pesadas
   - Usa `setTimeout` para máxima compatibilidad
   - Migra a `scheduler.postTask` cuando sea estable

3. **Monitorea en producción**
   - Integra RUM para métricas reales
   - Establece alertas para INP >200ms
   - A/B test diferentes estrategias

4. **Comparte vuestra experiencia**
   - ¿Has optimizado INP en vuestro proyecto?
   - ¿Qué técnica funcionó mejor para vuestro caso?
   - Hablemos en [LinkedIn](https://www.linkedin.com/in/joanleon/), [Bluesky](https://bsky.app/profile/nucliweb.net) o [X](https://x.com/nucliweb)

---

_Última actualización: 2026-02-18_
