Ver contenido
Guía de Rendimiento de JavaScript: Libera el Hilo Principal
Introducción
JavaScript es la causa más común de INP lento y de puntuaciones bajas en el audit "Minimize main thread work" de Lighthouse. Cada script que tu página carga debe descargarse, parsearse, compilarse y ejecutarse en el hilo principal—el mismo hilo que procesa los clics, el scroll y el tecleo del usuario. Esta guía cubre las técnicas concretas para reducir ese trabajo. El UXR SEO Analyzer reporta INP y señala JavaScript de terceros pesado en la pestaña "Performance".
A diferencia del CSS o las imágenes, JavaScript cuesta tiempo del hilo principal en cada etapa de su ciclo de vida—descarga, parseo, compilación y ejecución—así que un script que parece pequeño en la cascada de red puede seguir siendo costoso si hace mucho trabajo una vez que se ejecuta. Medir solo el tamaño del archivo pasa esto por alto; las técnicas de abajo apuntan al tiempo real de hilo principal en lugar del peso del archivo.
Cuándo Lighthouse Marca tu JavaScript
Chrome documenta umbrales explícitos para dos audits relacionados:
- Reduce JavaScript execution time: Lighthouse muestra una advertencia cuando la ejecución de JavaScript supera 2 segundos, y el audit falla cuando supera 3.5 segundos
- Minimize main thread work: Lighthouse marca páginas que mantienen el hilo principal ocupado más de 4 segundos durante la carga
- Reduce the impact of third-party code: Lighthouse marca scripts de terceros que bloquean el hilo principal por 250 ms o más
Estos tres números—2s, 3.5s, 4s y 250ms—son los objetivos concretos contra los que debes medir tu página, no umbrales arbitrarios.
Técnica 1: Code Splitting
En lugar de enviar un único bundle con todo el código de la aplicación, divide el JavaScript por ruta o por componente, para que el navegador solo descargue y ejecute lo que la página actual necesita:
// Antes: importación estática, todo en el bundle inicial
import Dashboard from './Dashboard';
// Después: importación dinámica, se carga solo cuando se necesita
const Dashboard = React.lazy(() => import('./Dashboard'));
Cada ruta que no visita el usuario nunca llega a descargarse, parsearse ni ejecutarse—eliminando ese trabajo del hilo principal por completo.
Técnica 2: Elimina Código No Utilizado
El "tree shaking" de los bundlers modernos elimina exports que ningún módulo importa, pero solo funciona bien con código en formato ES modules y sin efectos secundarios ocultos. Audita regularmente con el reporte de cobertura de Chrome DevTools (pestaña Coverage) para encontrar JavaScript que se descarga pero nunca se ejecuta.
Técnica 3: Usa Web Workers para Cómputo Pesado
Cualquier cálculo que no necesite tocar el DOM—parseo de datos grandes, procesamiento de imágenes, cálculos matemáticos—puede moverse a un Web Worker, que corre en un hilo separado y no bloquea la interactividad de la página:
// main.js
const worker = new Worker('data-processor.js');
worker.postMessage(largeDataset);
worker.onmessage = (e) => renderResults(e.data);
// data-processor.js — corre fuera del hilo principal
self.onmessage = (e) => {
const result = processHeavyComputation(e.data);
self.postMessage(result);
};
Técnica 4: Divide las Tareas Largas
La Long Animation Frames API de Chrome mide cuadros de más de 50ms y calcula un "blockingDuration"—la suma de la porción de cada tarea que excede ese umbral. Reducir o eliminar el blockingDuration dividiendo tareas largas en fragmentos más pequeños es clave para mejorar la responsividad medida por INP, incluso si la duración total del trabajo no cambia.
// Antes: una tarea larga bloquea el hilo principal
processAllItems(items);
// Después: se divide en fragmentos, cediendo el control entre cada uno
async function processInChunks(items) {
for (const chunk of chunksOf(items, 50)) {
processChunk(chunk);
await new Promise(resolve => setTimeout(resolve, 0)); // cede el hilo
}
}
Técnica 5: Debounce en Event Handlers Pesados
Los event handlers que se ejecutan en cada evento de scroll, resize o input pueden saturar el hilo principal. Usa debouncing o throttling para limitar cuántas veces se ejecuta la lógica pesada.
Auditando Terceros
Los scripts de terceros—analítica, chat en vivo, píxeles de anuncios—son una fuente frecuente de trabajo del hilo principal que no controlas directamente. Usa la pestaña Network y el panel Performance de Chrome DevTools para identificar cuáles bloquean el hilo por más de 250 ms y evalúa si pueden cargarse de forma diferida o eliminarse.
Una estrategia efectiva es el patrón "facade": renderizar un marcador de posición ligero (por ejemplo, una miniatura estática de un video incrustado) y cargar el script pesado del widget real solo cuando el usuario interactúa con él. Esto retrasa la ejecución de terceros hasta que realmente aporta valor, en lugar de competir con el contenido principal por el hilo desde el primer momento.
Costos Específicos de Frameworks
Los frameworks modernos añaden su propio trabajo al hilo principal más allá del código de la aplicación:
- Hidratación en frameworks con SSR/SSG (React, Vue): el navegador debe volver a ejecutar el árbol de componentes en el cliente para adjuntar event listeners, incluso cuando el HTML ya está renderizado. Esto puede dominar el trabajo del hilo principal en páginas con muchos componentes interactivos
- Re-renders innecesarios: componentes que se vuelven a renderizar sin que su salida cambie consumen ciclos de CPU sin beneficio; usa las herramientas de profiling de React o Vue DevTools para detectarlos
- Bibliotecas de utilidades completas: importar una librería entera para usar una sola función infla el bundle inicial; prefiere imports específicos cuando el bundler lo permite
La hidratación selectiva o parcial—hidratar solo los componentes interactivos visibles, en lugar de todo el árbol de una vez—es la técnica más efectiva para reducir este costo en aplicaciones grandes con SSR.
Ejemplo Práctico
Una página de listado reporta un INP de 620 ms—en la zona "Pobre". El desglose de la interacción más lenta (abrir un panel de filtros) muestra: retraso de entrada 40 ms, duración de procesamiento 540 ms, retraso de presentación 40 ms. La duración de procesamiento domina, así que el problema está en el event handler del filtro, no en trabajo previo del hilo principal.
Perfilando con el panel Performance se descubre que el handler recalcula el conteo de resultados para las 40 categorías del filtro en cada clic, de forma síncrona. La solución: dividir ese recálculo en fragmentos que cedan el hilo cada 50ms, y memorizar los conteos que no cambiaron desde el último render. El INP baja a la zona "Buena" sin tocar ninguna otra parte del código.
Checklist de Rendimiento de JavaScript
- [ ] JavaScript dividido por ruta con importaciones dinámicas
- [ ] Código no utilizado eliminado (verificado con el reporte de Coverage)
- [ ] Cómputo pesado movido a Web Workers cuando no requiere el DOM
- [ ] Tareas largas divididas en fragmentos que ceden el hilo principal
- [ ] Event handlers de scroll/resize/input con debounce o throttle
- [ ] Scripts de terceros auditados; ninguno bloquea el hilo por más de 250 ms
Preguntas Frecuentes
¿Code splitting mejora LCP o solo INP?
Principalmente INP y el tiempo de ejecución de JavaScript. Sin embargo, si el elemento LCP depende de que un componente de React se hidrate, reducir el bundle inicial también puede adelantar ese renderizado.
¿Cuántos fragmentos debo usar al dividir una tarea larga?
No hay un número fijo—el objetivo es que ningún fragmento individual supere los 50ms que define la Long Animation Frames API como umbral de tarea larga. Ajusta el tamaño del fragmento según el hardware de tus usuarios reales.
¿Los Web Workers pueden acceder al DOM?
No directamente. Los Web Workers corren en un contexto global separado sin acceso al DOM; deben comunicarse con el hilo principal mediante mensajes (postMessage) para que este actualice la interfaz.
¿El patrón facade sirve para todos los scripts de terceros?
Funciona mejor para widgets claramente interactivos—videos incrustados, chat en vivo, mapas. Para scripts de analítica que deben ejecutarse en la carga inicial, la estrategia correcta es cargarlos con async y con la prioridad de red más baja posible, no diferir su ejecución por completo.
Guías Relacionadas
Referencias
- Chrome Developers - Reduce JavaScript execution time
- Chrome Developers - Minimize main thread work
- Chrome Developers - Reduce the impact of third-party code
- Chrome Developers - Long Animation Frames API
