Rendimiento y capacidad de respuesta en JavaScript | Nicolás Garzón
Rendimiento no significa únicamente “terminar rápido”. Una aplicación también debe responder a entrada, renderizar a tiempo y utilizar recursos de forma sostenible.
Texto
Copiar throughput → cuánto trabajo completa
latency → cuánto tarda una operación
responsiveness → qué tan pronto responde al usuario
memory → cuánto conserva
network → cuánto descarga y espera
En el navegador, JavaScript, eventos, DOM y gran parte del renderizado comparten el hilo principal.
Clicks.
Teclado.
Scroll procesado por JS.
Animaciones.
Pintura.
Una callback que ocupa decenas o cientos de milisegundos puede ser visible para el usuario.
JavaScript
Copiar button. addEventListener ( "click" , ( ) => {
calculateLargeReport ( ) ;
} ) ; Aunque el cálculo total sea correcto, la interacción se siente congelada.
JavaScript
Copiar async function processInChunks ( items ) {
for ( let index = 0 ; index < items. length; ) {
const deadline = performance. now ( ) + 8 ;
while (
index < items. length &&
performance. now ( ) < deadline
) {
processItem ( items[ index] ) ;
index++ ;
}
await new Promise ( ( resolve ) => {
setTimeout ( resolve, 0 ) ;
} ) ;
}
} Ceder crea oportunidades de entrada y renderizado. El tamaño apropiado depende del dispositivo y trabajo.
Runtimes modernos pueden ofrecer APIs como scheduler.yield() o scheduler.postTask() para planificación con intención y prioridades.
Comprueba soporte y ofrece fallback; no pertenecen al núcleo de ECMAScript.
JavaScript
Copiar requestAnimationFrame ( ( ) => {
updateVisualState ( ) ;
} ) ; Alinea trabajo visual con una oportunidad de frame. No debe contener cálculos largos.
Puede ejecutar trabajo de baja prioridad cuando existe tiempo libre.
No está disponible en todos los navegadores y no garantiza cuándo ocurrirá. No la uses para operaciones esenciales o con deadline estricto.
Un worker puede mover cálculo CPU-intensivo fuera del hilo principal.
Serialización o transferencia.
Mensajes.
Memoria.
Creación del worker.
Mide el punto de equilibrio.
JavaScript
Copiar for ( const product of products) {
const category = categories. find (
( candidate ) => candidate. id === product. categoryId,
) ;
} Puede realizar muchas búsquedas repetidas.
JavaScript
Copiar const categoriesById = new Map (
categories. map ( ( category ) => [ category. id, category] ) ,
) ; Después cada consulta expresa un índice directo.
La elección de estructura suele importar más que microoptimizar sintaxis.
La optimización más efectiva suele ser no hacer trabajo innecesario:
No volver a parsear.
No volver a solicitar.
No renderizar elementos fuera del viewport.
No recalcular valores invariantes.
No convertir estructuras varias veces.
JavaScript
Copiar function memoizeOne ( operation ) {
let previousArgument;
let previousResult;
let initialized = false ;
return ( argument ) => {
if (
initialized &&
Object. is ( argument, previousArgument)
) {
return previousResult;
}
initialized = true ;
previousArgument = argument;
previousResult = operation ( argument) ;
return previousResult;
} ;
} Memoizar intercambia memoria por cálculo. Solo aporta cuando:
La operación es costosa.
Las entradas se repiten.
La invalidación es correcta.
Una cache rápida puede devolver datos obsoletos. Define:
Clave.
Tiempo de vida.
Invalidación.
Tamaño.
Fuente de verdad.
Agrupa cambios y evita mezclar lecturas geométricas con escrituras.
JavaScript
Copiar const widths = items. map (
( item ) => item. getBoundingClientRect ( ) . width,
) ;
items. forEach ( ( item, index ) => {
item. style. width = ` ${ widths[ index] + 10 } px ` ;
} ) ; Renderizar diez mil filas puede ser costoso aunque los datos ya estén en memoria. Una lista virtual muestra solo la ventana visible y un buffer.
Alturas.
Focus.
Navegación de teclado.
Accesibilidad.
Scroll.
Rendimiento también depende de:
Tamaño de JavaScript.
Cantidad de requests.
Cache HTTP.
Compresión.
Latencia del servidor.
Imágenes y fuentes.
Una función rápida no compensa un bundle enorme descargado antes de iniciar.
JavaScript
Copiar const editor = await import ( "./editor.js" ) ; Reduce carga inicial cuando la función es opcional. Demasiados chunks pueden aumentar overhead de red y coordinación.
Debounce reduce trabajo al finalizar una ráfaga. Throttle limita frecuencia.
No los uses para ocultar una operación que sigue siendo demasiado costosa cuando finalmente se ejecuta.
Mostrar estado de carga inmediato.
Mantener controles utilizables.
Renderizar contenido progresivamente.
Evitar layout shifts.
Preservar foco y contexto.
Una operación de dos segundos con feedback puede sentirse mejor que una de un segundo con interfaz congelada.
Código menos legible no es automáticamente más rápido.
Define una métrica.
Reproduce la carga real.
Perfila.
Cambia una causa.
Vuelve a medir.
JavaScript
Copiar const search = debounce ( async ( query ) => {
controller?. abort ( ) ;
controller = new AbortController ( ) ;
const products = await loadProducts ( query, {
signal : controller. signal,
} ) ;
renderProducts ( products) ;
} , 250 ) ; Reduce requests durante escritura y cancela resultados obsoletos.
Medir solo tiempo total e ignorar interacción.
Mover cualquier código a un worker.
Memoizar funciones baratas.
Crear caches sin invalidación.
Microoptimizar sintaxis antes que algoritmo.
Usar debounce como solución de una función lenta.
Leer y escribir layout alternadamente.
Cargar toda función opcional al inicio.
Probar solo en un equipo potente.
Sacrificar accesibilidad por virtualización o animación.
Rendimiento incluye latencia, throughput y capacidad de respuesta.
El hilo principal debe quedar disponible.
Divide o mueve cálculos largos.
La estructura de datos puede reducir complejidad.
Evitar trabajo supera a ejecutarlo un poco más rápido.
Memoization y caches tienen costo de memoria y frescura.
DOM, red y bundle también forman parte del rendimiento.
Optimiza a partir de medición real.
¿Por qué una función que termina en 200 ms puede seguir siendo un problema en el navegador?
Respuesta Porque si ocupa el hilo principal durante esos 200 ms, puede retrasar entrada, animación y renderizado, haciendo que la interfaz se sienta bloqueada.
Medición, profiling y optimización explica cómo encontrar evidencia antes de modificar código.