Skip to content

Yield to Main: setTimeout vs queueMicrotask vs scheduler.postTask

Published:

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

Open Tabla de Contenidos

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:

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.

// ❌ 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:

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

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
function yieldWithSetTimeout() {
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

Ejemplo práctico con medición

// ❌ 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:

Medición con Long Task API

// 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:

❌ Contras:

Caso de uso ideal

Usa setTimeout(fn, 0) cuando:

// 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.

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

// 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?

// 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

// ❌ 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

// ❌ 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

// ❌ 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:

// ✅ 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.

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

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

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

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

// 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

// 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):

Caso de uso ideal

Usa scheduler.postTask() cuando:

// 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

CaracterísticasetTimeout(fn, 0)queueMicrotask(fn)scheduler.postTask(fn)
Tipo de colaMacrotask QueueMicrotask QueueTask Queue (prioritizada)
Cuándo ejecutaDespués de renderAntes de renderConfigurable 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íaMedioBajoMedio-Alto
Ideal paraChunking generalState batchingProgressive rendering

Benchmark: Las tres técnicas

// 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

// 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

/**
 * 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

/**
 * 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)

/**
 * 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

/**
 * 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

// 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

// 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)

// 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?

    • → Descarta queueMicrotask, considera setTimeout o scheduler.postTask
    • NOqueueMicrotask es válido para batching de estado
  2. ¿Necesitas control de prioridad?

    • scheduler.postTask (con fallback a setTimeout)
    • NOsetTimeout(fn, 0) es suficiente
  3. ¿Qué tan crítico es el soporte de navegadores?

    • UniversalsetTimeout (funciona en todos)
    • Modernoscheduler.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?

    • → Usa user-blocking priority
    • NO → Usa user-visible o background priority

Tabla de decisión rápida

Caso de usoTécnica recomendadaAlternativa
Procesar array grandesetTimeout + chunkingscheduler.postTask (background)
Renderizar lista progresivascheduler.postTask (user-visible)setTimeout + chunking
Responder a clickscheduler.postTask (user-blocking)Código síncrono si <50ms
Batch de setStatequeueMicrotaskPromise.resolve().then()
Import de CSVsetTimeout + progressscheduler.postTask (background)
Animación frame-by-framerequestAnimationFrameN/A (no yield)
Analytics / trackingscheduler.postTask (background)setTimeout con delay
Preload de recursosrequestIdleCallbackscheduler.postTask (background)
Validación de formularioCódigo síncronoscheduler.postTask (user-blocking)
Search indexingscheduler.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

queueMicrotask(fn) - El rápido pero peligroso

scheduler.postTask(fn) - El moderno y poderoso

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

// 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:

Web Performance:

Event Loop:

Visualizadores:

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, Bluesky o X

Última actualización: 2026-02-18


Previous Post
Guía práctica del elemento <img>: de lo básico a LCP
Next Post
Optimización de scripts de consentimiento para Core Web Vitals