Optimización de velocidad web

Cómo reducir el tiempo de ejecución de JavaScript y dividir las tareas largas

Descargar menos ayuda, pero la respuesta del sitio también depende de tareas más breves y menos trabajo inicial.

Un bloque largo del hilo principal dividido en bloques pequeños separados por espacios
Ilustración: las tareas más breves dejan oportunidades para procesar entradas y renderizar.

Reduce el tiempo de ejecución de JavaScript identificando funciones costosas, eliminando trabajo innecesario y cargando las funciones solo cuando hacen falta. Si el trabajo necesario sigue ocupando demasiado tiempo el hilo principal, divídelo en tareas independientes o traslada los cálculos adecuados a un web worker. El tamaño descargado, el tiempo total de CPU y la tarea ininterrumpida más larga son problemas distintos: mide los tres antes de elegir la solución.

Graba la interacción lenta antes de modificar el paquete

Utiliza una compilación de producción y una acción reproducible: abrir la navegación, filtrar productos o desplegar un informe. Graba la carga inicial y la interacción por separado en el panel Performance de Chrome DevTools. Mantén constantes el dispositivo, la limitación de CPU, la red y el consentimiento. Las extensiones y la instrumentación de desarrollo pueden alterar la grabación; utiliza un perfil limpio y documenta el entorno.

Selecciona el intervalo en que la interfaz deja de responder. Examina la pista Main, utiliza Bottom-up para localizar funciones con mucho tiempo propio y Call tree para entender quién las invoca. El tiempo propio corresponde a la función; el total incluye sus descendientes. Un pequeño envoltorio con un total elevado puede conducir al problema sin ser la función que debes reescribir. Los mapas de código fuente permiten relacionar el paquete con el módulo original.

Según la documentación de web.dev, una tarea larga dura más de 50 milisegundos. Ese límite identifica bloqueos; no es un presupuesto que todas las tareas deban agotar. Varias tareas más cortas también pueden perjudicar la experiencia, especialmente si compiten con renderizado y scripts externos. Examina la interacción, no solo los indicadores de tareas largas.

Los diagnósticos de JavaScript y el treemap de Lighthouse orientan hacia recursos costosos y dependencias grandes. No demuestran que el archivo mayor causara la pausa. Compara tamaño y ejecución, y consulta la guía para mejorar INP cuando el retraso abarque la entrada, su procesamiento y el siguiente pintado.

Distingue entre eliminar, aplazar y programar trabajo

Elige la intervención según las pruebas. La compresión reduce los bytes transferidos, pero el navegador recibe el programa descomprimido. La minificación mejora la entrega sin sustituir la reducción del trabajo ejecutado.

Estos cambios resuelven problemas diferentes y deben evaluarse por separado.

Evidencia Primer cambio que investigar Qué no garantiza
Código innecesario en la ruta inicial Eliminarlo o crear un fragmento independiente Un primer uso más rápido de esa función
Inicializaciones o escuchadores duplicados Eliminar las repeticiones Menos bytes transferidos
Cálculos largos independientes del DOM Worker o un algoritmo mejor Menor consumo total de CPU
Una secuencia necesaria bloquea la respuesta visual Ceder el control entre pasos acotados Finalizar antes toda la secuencia
JavaScript provoca mucho trabajo de estilos o disposición Reducir cambios del DOM y dependencias de disposición Resolverlo cambiando atributos del script

Antes de editar, formula una hipótesis: qué función debe ejecutarse menos, qué fragmento debe desaparecer de la carga inicial o dónde debería poder renderizar el navegador. La siguiente grabación servirá para explicar el cambio, no solo para comparar puntuaciones.

Envía únicamente el código que necesita la vista actual

Inspecciona la compilación de producción con el analizador del empaquetador o el treemap de Lighthouse. Busca versiones duplicadas, datos de idiomas sin utilizar, bibliotecas grandes importadas para un pequeño auxiliar y funciones incluidas en todas las páginas. Investiga cómo entró cada dependencia antes de sustituirla. Una alternativa más pequeña puede cambiar comportamientos importantes o aumentar el mantenimiento.

El tree shaking elimina exportaciones sin uso identificables estáticamente cuando la compilación lo permite. Funciona mejor con módulos ES y declaraciones correctas de efectos secundarios. No declares todos los paquetes libres de efectos secundarios para reducir el tamaño. Un módulo que instala un polyfill, registra un componente o importa CSS imprescindible puede necesitar ejecutarse aunque no se utilicen sus exportaciones. Comprueba el resultado de producción, no solo la instrucción import.

Coverage aporta otra pista: el código sin uso en una grabación puede necesitarse al iniciar sesión, cambiar de ruta o abrir un diálogo. Recorre esas situaciones antes de eliminarlo. Incluye errores y accesibilidad en la revisión; un uso infrecuente no hace innecesaria una función. Es preferible retirar una función abandonada que aplicar transformaciones frágiles a código necesario.

Carga una función opcional con importación dinámica

Una importación dinámica establece un punto de carga separado si el empaquetador admite división de código. Sitúalo alrededor de una función solicitada de forma independiente, como una vista previa de exportación. Dividir cada función pequeña añade coordinación y solicitudes; dividir una ruta completa puede ser demasiado amplio para un componente poco usado.

Este ejemplo utiliza tres archivos servidos mediante HTTP y carga un módulo tras activar el botón. La aplicación debe localizar la etiqueta, el progreso y los errores.

html
<!-- index.html -->
<button id="preview" type="button">Preview export</button>
<p id="status" role="status"></p>
<pre id="output"></pre>
<script type="module" src="/main.js"></script>

El módulo de entrada restaura el control incluso si falla la importación.

js
// main.js
const button = document.querySelector('#preview');
const status = document.querySelector('#status');
const output = document.querySelector('#output');
button.addEventListener('click', async () => {
  button.disabled = true;
  status.textContent = 'Loading preview';
  try {
    const { renderPreview } = await import('./preview.js');
    renderPreview(output);
    status.textContent = 'Preview ready';
  } catch {
    status.textContent = 'Preview unavailable. Try again.';
  } finally {
    button.disabled = false;
  }
});

El módulo independiente proporciona la función opcional.

js
// preview.js
export function renderPreview(element) {
  element.textContent = JSON.stringify({ format: 'csv', ready: true }, null, 2);
}

Es un ejemplo funcional de carga, no una carga de trabajo costosa ni una optimización medida. El código real puede inicializarse de forma costosa al importarse. Prueba su primera activación con caché fría y las siguientes. Si el primer uso resulta lento, mejora la función o anticipa su descarga prudentemente cuando la intención sea clara; no traslades el problema inicial al primer clic.

Deja oportunidades reales para responder

Invocar varias funciones pequeñas sincrónicamente sigue formando una única tarea. Esperar una promesa ya resuelta tampoco crea de forma fiable una oportunidad de renderizado: sus continuaciones son microtareas. Utiliza un mecanismo que continúe en una tarea futura cuando la operación pueda pausarse sin alterar su corrección.

Este auxiliar prefiere scheduler.yield() si está disponible y utiliza un temporizador como alternativa; el bucle limita cada lote.

js
function yieldToBrowser() {
  if (globalThis.scheduler?.yield) return globalThis.scheduler.yield();
  return new Promise(resolve => setTimeout(resolve, 0));
}

export async function normalizeRows(rows, batchSize = 100) {
  if (!Number.isInteger(batchSize) || batchSize < 1) {
    throw new RangeError('batchSize must be a positive integer');
  }
  const normalized = [];
  for (let start = 0; start < rows.length; start += batchSize) {
    const end = Math.min(start + batchSize, rows.length);
    for (let index = start; index < end; index++) {
      normalized.push(String(rows[index]).trim());
    }
    if (end < rows.length) await yieldToBrowser();
  }
  return normalized;
}

El tamaño del lote es una configuración ilustrativa, no un límite seguro universal. Un único elemento costoso puede crear una tarea larga. Ajusta según el trabajo y los dispositivos, y considera cancelar si el usuario cambia los datos durante el procesamiento. La alternativa con temporizador no conserva la prioridad de programación de la API nativa: prueba ambas rutas. La documentación oficial sobre tareas largas explica el modelo.

Ceder el control puede mejorar la respuesta sin reducir el tiempo total, e incluso añadir sobrecarga. Conserva las operaciones que deban ser atómicas y no anuncies que se guardó algo antes de terminar. La respuesta visual debe describir el estado real, por ejemplo, procesamiento en curso.

Traslada los cálculos independientes a un web worker

Los workers ejecutan JavaScript fuera del hilo principal de la ventana, pero no manipulan directamente su DOM. Son adecuados para análisis, transformaciones y cálculos independientes. No solucionan automáticamente tareas dominadas por insertar elementos, medir la disposición o pintar. Una operación diminuta puede salir más cara en un worker por su inicio y mensajería.

Este worker de módulo muestra la separación; los datos son deliberadamente demasiado pequeños para sugerir una mejora de rendimiento.

js
// totals.worker.js
self.addEventListener('message', event => {
  const total = event.data.reduce((sum, value) => sum + value, 0);
  self.postMessage(total);
});

El llamador recibe el resultado en el hilo principal y libera después el worker.

js
// totals.js, loaded as a module
const worker = new Worker(new URL('./totals.worker.js', import.meta.url), {
  type: 'module'
});
worker.addEventListener('message', event => {
  console.log('Total:', event.data);
  worker.terminate();
});
worker.addEventListener('error', () => worker.terminate());
worker.postMessage([2, 4, 6]);

En un flujo real, define validación de entrada, errores y cancelación. Reutiliza el worker si los trabajos repetidos justifican mantenerlo. Los mensajes grandes pueden copiar datos; los búferes transferibles evitan algunas copias, pero transfieren su propiedad y cambian lo que el emisor puede hacer después. Mide el tiempo completo, la disponibilidad del hilo principal y la memoria antes de aceptar el cambio.

Aplaza el inicio sin ocultar un cuello de botella posterior

Un script aplazado sigue ejecutándose en el hilo principal. async modifica el momento de carga y ejecución; defer también conserva el orden de scripts clásicos externos después del análisis del documento. Ninguno divide una función costosa. Consulta la guía de recursos que bloquean el renderizado para las dependencias de carga y vuelve a la grabación para evaluar la ejecución.

Reserva la programación en inactividad para trabajo opcional. requestIdleCallback() tiene disponibilidad limitada y el navegador puede aplazar la llamada. Detecta la API, prepara una alternativa y decide si corresponde un tiempo límite. La navegación, validación y respuesta imprescindibles no deben depender de que aparezca tiempo libre. Un tiempo límite tampoco abarata una función grande: el trabajo debe seguir acotado.

El HTML generado en el servidor puede mostrarse antes de que el framework conecte sus comportamientos interactivos. La hidratación puede consumir mucha CPU: ver controles pronto no demuestra que respondan. Limita los componentes cliente, evita inicializaciones duplicadas y utiliza hidratación selectiva o aplazada admitida por el framework solo si conserva el comportamiento. Prueba clics durante el inicio, teclado, foco y primer uso de cada región.

El código externo comparte esas oportunidades de ejecución. Revisa el coste de los scripts de terceros y elimina etiquetas duplicadas antes de añadir programación compleja. Documenta el propósito y la condición de activación para evitar que futuras incorporaciones restauren el problema.

Rastrea repeticiones y renderizado innecesario

El problema no siempre es un algoritmo costoso. Una operación económica puede repetirse con cambios de estado irrelevantes, cada evento de desplazamiento o cada montaje. Cuenta invocaciones y relaciónalas con la acción del usuario. Corregir una suscripción o un escuchador duplicado puede ser más duradero que reescribir su función.

En una lista interactiva, separa transformación de datos y actualización visual. Recalcula únicamente cuando cambien las entradas y evita reconstruir toda la lista para modificar una región. La memoización conviene cuando el trabajo evitado compensa memoria e invalidación. Dependencias incorrectas pueden mostrar resultados antiguos: acompaña la comparación de rendimiento con pruebas de corrección.

Busca secuencias que escriben estilos y leen geometría repetidamente. Esas lecturas pueden forzar cálculos de disposición antes de continuar JavaScript. Cuando sea posible, recoge las medidas antes de aplicar las escrituras. Una función rápida puede provocar repetidamente trabajo costoso de estilos y disposición en otra parte de la grabación.

Retira intervalos, observadores y suscripciones que ya no necesita el componente. Una grabación inmediatamente después de cargar puede omitir trabajo acumulado durante la navegación. Repite el recorrido varias veces sin recargar y comprueba si crece el procesamiento. Es una prueba diagnóstica, no una acusación de fugas contra un framework.

Comprueba el mismo recorrido después del cambio

Repite la grabación con condiciones equivalentes y prueba una carga fría de la función, una visita repetida y un dispositivo más lento. Compara tareas largas y respuesta visible, no solo bytes. Un inicio más corto puede coexistir con una primera interacción peor; un callback más rápido todavía puede provocar renderizado costoso.

Registra el cambio, las pruebas y la reversión en la lista de comprobación de rendimiento. Utiliza la colección de optimización de velocidad para el trabajo posterior y elige la siguiente función costosa de la nueva grabación. Conserva el cambio cuando mejore el recorrido original y sigan funcionando carga, errores y accesibilidad.

Guías relacionadas