Profiling y optimización de JavaScript | Nicolás Garzón
Optimizar consiste en mejorar una métrica relevante sin romper comportamiento, mantenibilidad u otras restricciones.
Texto
Copiar problema observado
↓
métrica
↓
perfil
↓
hipótesis
↓
cambio
↓
comparación
Tiempo hasta mostrar resultados.
Duración de una interacción.
Frames perdidos.
Uso máximo de memoria.
Tamaño de bundle.
Solicitudes por acción.
Tiempo de CPU de una función.
“Que sea más rápido” no permite saber si el cambio funcionó.
JavaScript
Copiar const start = performance. now ( ) ;
operation ( ) ;
const duration = performance. now ( ) - start; Es monotónico y apropiado para medir duraciones dentro de una sesión.
Una sola ejecución incluye ruido de:
JIT.
Cache.
GC.
Sistema operativo.
Otras pestañas.
JavaScript
Copiar performance. mark ( "checkout:start" ) ;
await checkout ( ) ;
performance. mark ( "checkout:end" ) ;
performance. measure (
"checkout" ,
"checkout:start" ,
"checkout:end" ,
) ; Permite registrar hitos y observarlos con herramientas.
JavaScript
Copiar console. time ( "load" ) ;
await load ( ) ;
console. timeEnd ( "load" ) ; Útil para una inspección rápida, pero no sustituye un perfil completo.
En DevTools puedes observar:
Main thread.
Tasks y long tasks.
Call tree.
Bottom-up.
Frames.
Rendering.
Network.
Timings.
Busca dónde se consume tiempo, no solo dónde termina el síntoma.
Un perfil de CPU muestrea o instrumenta stacks para mostrar funciones costosas.
Muestra la ruta desde llamadas superiores.
Agrupa costo por función sin importar desde dónde se llamó.
Tiempo dentro de la función excluyendo llamadas hijas.
Incluye funciones llamadas.
Una función con total alto y self bajo puede ser solo la coordinadora de trabajo costoso.
Waterfall.
DNS, conexión y TLS.
Waiting/TTFB.
Transferencia.
Cache.
Prioridad.
Payload.
Solicitudes duplicadas.
No atribuyas a JavaScript una latencia que pertenece al servidor o red.
Permiten comparar objetos y rutas de retención.
Shallow size.
Retained size.
Dominators.
Retaining path.
Detached DOM trees.
Una gran retained size indica cuánto podría liberarse si ese objeto dejara de ser alcanzable.
Muestra qué rutas crean objetos y cuáles permanecen.
Útil cuando el problema es:
Churn de objetos.
GC frecuente.
Colecciones que crecen.
Eventos repetidos.
JavaScript
Copiar const observer = new PerformanceObserver ( ( list ) => {
for ( const entry of list. getEntries ( ) ) {
reportMetric ( entry) ;
}
} ) ;
observer. observe ( {
type : "longtask" ,
buffered : true ,
} ) ; Los entry types disponibles dependen del navegador y contexto.
JavaScript
Copiar performance. mark ( "search:start" , {
detail : {
queryLength : query. length,
} ,
} ) ; No incluyas datos sensibles en marcas o telemetría.
JavaScript
Copiar for ( let index = 0 ; index < 1_000_000 ; index++ ) {
operation ( ) ;
} Pueden medir algo distinto al uso real por:
Dead code elimination.
Warm-up.
Inputs constantes.
Ausencia de DOM o I/O.
Optimización específica del benchmark.
Úsalos para preguntas estrechas y valida después en el flujo real.
Las primeras ejecuciones pueden incluir compilación e inicialización. Las posteriores pueden utilizar código optimizado.
No descartes siempre el warm-up: la experiencia inicial también importa para usuarios.
Una prueba puede parecer más lenta porque coincide con una pausa de GC. Eso puede ser parte real del costo si el flujo crea mucha basura.
No fuerces GC en producción ni lo uses para “arreglar” una fuga.
Mismo entorno.
Mismos datos.
Varias muestras.
Percentiles, no solo promedio.
Producción o build comparable.
Dispositivos representativos.
Sin extensiones o ruido innecesario cuando haces laboratorio.
Texto
Copiar p50 → experiencia mediana
p75 → mayoría de usuarios
p95 → cola lenta
p99 → casos extremosEl promedio puede ocultar una parte de usuarios con experiencia muy mala.
Control, reproducibilidad y profiling detallado.
Dispositivos, redes y datos reales.
Necesitas ambas: el laboratorio explica; el campo muestra impacto.
Texto
Copiar bundle < 200 KB gzip
interaction p75 < 200 ms
memory after navigation < thresholdUn presupuesto vuelve detectable una regresión antes de acumularse.
Si modificas algoritmo, caché, worker y rendering al mismo tiempo, no sabrás qué produjo la mejora o regresión.
Cambia una hipótesis y compara.
JavaScript
Copiar performance. mark ( "load:start" ) ;
const products = await loadProducts ( ) ;
performance. mark ( "load:end" ) ;
performance. mark ( "render:start" ) ;
renderProducts ( products) ;
performance. mark ( "render:end" ) ;
performance. measure (
"load-products" ,
"load:start" ,
"load:end" ,
) ;
performance. measure (
"render-products" ,
"render:start" ,
"render:end" ,
) ; Separar fases muestra si el costo está en red o renderizado.
Optimizar sin una métrica.
Confiar en una sola ejecución.
Medir desarrollo en vez de producción.
Usar microbenchmarks como prueba definitiva.
Confundir total time y self time.
Ignorar red o layout.
Mirar cantidad de objetos sin rutas de retención.
Utilizar promedio e ignorar cola lenta.
Cambiar varias causas a la vez.
Publicar telemetría con información sensible.
Define una métrica antes de optimizar.
performance.now (se abre en otra pestaña) mide duraciones monotónicas.
Profiles muestran dónde se consume CPU.
Network panel separa red de ejecución.
Heap snapshots muestran retención.
Los benchmarks necesitan contexto y varias muestras.
Campo y laboratorio responden preguntas distintas.
Percentiles muestran la cola lenta.
Vuelve a medir después de cada cambio.
¿Por qué una función con self time bajo puede aparecer como muy costosa en total time?
Respuesta Porque coordina llamadas a otras funciones costosas. Su propio cuerpo consume poco, pero el trabajo descendiente se suma a su total.
Motores JavaScript, interpretación y JIT explica cómo un motor lleva el código fuente hasta ejecución optimizada.