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:
- 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.
// ❌ 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:
- Call Stack: Código síncrono que se ejecuta inmediatamente
- Microtask Queue: Promesas,
queueMicrotask()- ejecutan antes del render - Macrotask Queue:
setTimeout, eventos, I/O - ejecutan después del render - 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:
- El call stack esté vacío
- Todas las microtasks se hayan procesado
- 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:
- 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
// 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
// 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:
- Después del código síncrono actual
- Antes de cualquier macrotask (
setTimeout, eventos) - 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:
- 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
// ✅ 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):
- 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)
// 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í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 |
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:
- Abre DevTools
- Ve a la pestaña “Console”
- Copia y pega el snippet
- Presiona Enter
- 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
-
¿Necesitas yield to main para mejorar INP?
- SÍ → Descarta
queueMicrotask, considerasetTimeoutoscheduler.postTask - NO →
queueMicrotaskes válido para batching de estado
- SÍ → Descarta
-
¿Necesitas control de prioridad?
- SÍ →
scheduler.postTask(con fallback asetTimeout) - NO →
setTimeout(fn, 0)es suficiente
- SÍ →
-
¿Qué tan crítico es el soporte de navegadores?
- Universal →
setTimeout(funciona en todos) - Moderno →
scheduler.postTaskcon polyfill
- Universal →
-
¿Cuánto dura la tarea?
- <50ms → No necesitas yield, ejecuta directamente
- 50-200ms → Divide en 2-4 chunks con
setTimeout - >200ms → Usa
scheduler.postTaskcon prioridades
-
¿La tarea bloquea interacciones de usuarios y usuarias?
- SÍ → Usa
user-blockingpriority - NO → Usa
user-visibleobackgroundpriority
- SÍ → Usa
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
-
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
-
Divide tareas grandes en chunks de 16-50ms
- 16ms = 1 frame a 60fps
- 50ms = threshold de long task
- Usa
yieldToMain()entre chunks
-
Prioriza el trabajo crítico
- First paint:
user-blocking - Contenido visible:
user-visible - Analytics/tracking:
background
- First paint:
-
No abuses de microtasks
queueMicrotaskno mejora INP- Úsalo solo para consistency de estado
- Cuidado con loops infinitos
-
Ten un plan de fallback
- Polyfill para
scheduler.postTask - Feature detection siempre
setTimeoutcomo red de seguridad
- Polyfill para
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:
-
Audita el código actual
- Ejecuta el snippet de DevTools incluido
- Identifica long tasks con Performance Observer
- Mide INP real con Web Vitals
-
Implementa yielding progresivamente
- Empieza por las tareas más pesadas
- Usa
setTimeoutpara máxima compatibilidad - Migra a
scheduler.postTaskcuando sea estable
-
Monitorea en producción
- Integra RUM para métricas reales
- Establece alertas para INP >200ms
- A/B test diferentes estrategias
-
Comparte vuestra experiencia
Última actualización: 2026-02-18