Optimización del rendimiento en JavaScript
Técnicas y buenas prácticas para crear aplicaciones JavaScript rápidas y eficientes.
InSkillCoach
Optimización del rendimiento en JavaScript
El rendimiento es un aspecto crucial del desarrollo web moderno. Las aplicaciones lentas frustran a los usuarios y pueden tener un impacto negativo en las conversiones, el posicionamiento SEO y la experiencia global. Este artículo explora las técnicas de optimización del rendimiento en JavaScript, desde los fundamentos hasta las estrategias avanzadas.
¿Por qué es importante el rendimiento?
- Experiencia de usuario: los usuarios abandonan los sitios lentos (el 53 % abandona una página que tarda más de 3 segundos en cargar)
- SEO: Google utiliza la velocidad como factor de posicionamiento
- Conversión: cada segundo de retraso puede reducir las conversiones un 7 %
- Accesibilidad: los dispositivos y las conexiones de gama baja se benefician enormemente de las optimizaciones
- Costes: un código eficiente consume menos recursos de servidor
Entender el rendimiento de JavaScript
El motor de JavaScript
Para optimizar de forma eficaz, conviene entender cómo funciona el motor de JavaScript:
- Parsing: transformación del código en un Abstract Syntax Tree (AST)
- Compilación: conversión a bytecode y después a código máquina optimizado
- Ejecución: ejecución del código con recopilación de datos para futuras optimizaciones
- Garbage Collection: liberación de la memoria no utilizada
Métricas de rendimiento clave
- First Contentful Paint (FCP): primera aparición de contenido
- Largest Contentful Paint (LCP): aparición del contenido principal
- First Input Delay (FID): retraso antes de la interacción
- Cumulative Layout Shift (CLS): estabilidad visual
- Time to Interactive (TTI): momento en que la página se vuelve interactiva
- Total Blocking Time (TBT): tiempo durante el cual el hilo principal está bloqueado
Optimizaciones fundamentales
1. Minimizar las manipulaciones del DOM
El DOM es lento. Cada vez que modificas el DOM, el navegador debe recalcular los estilos, el diseño de la página y, en ocasiones, repintar la pantalla.
// Inefectivo: múltiples manipulaciones del DOM
for (let i = 0; i < 1000; i++) {
document.body.innerHTML += `<div>${i}</div>`;
}
// Optimizado: una única manipulación del DOM
let html = '';
for (let i = 0; i < 1000; i++) {
html += `<div>${i}</div>`;
}
document.body.innerHTML += html;
// Aún mejor: usar DocumentFragment
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const div = document.createElement('div');
div.textContent = i;
fragment.appendChild(div);
}
document.body.appendChild(fragment);
2. Evitar los reflows y repaints
- Reflow: recálculo completo de la geometría de los elementos (costoso)
- Repaint: actualización visual sin cambio de geometría (menos costoso)
// Inefectivo: provoca varios reflows
const element = document.getElementById('monElement');
element.style.width = '100px';
element.style.height = '200px';
element.style.margin = '10px';
// Optimizado: un solo reflow
const element = document.getElementById('monElement');
element.classList.add('nouvelles-dimensions');
// O con style
element.style.cssText = 'width: 100px; height: 200px; margin: 10px;';
3. Utilizar los eventos de forma eficiente
// Inefectivo: un manejador por elemento
document.querySelectorAll('button').forEach(button => {
button.addEventListener('click', handleClick);
});
// Optimizado: delegación de eventos
document.addEventListener('click', e => {
if (e.target.matches('button')) {
handleClick(e);
}
});
4. Optimizar los bucles
const arr = new Array(1000000).fill(1);
// Inefectivo: acceso a la longitud en cada iteración
for (let i = 0; i < arr.length; i++) {
// procesamiento
}
// Optimizado: almacenamiento de la longitud
const len = arr.length;
for (let i = 0; i < len; i++) {
// procesamiento
}
// Moderno: métodos de array
arr.forEach(item => {
// procesamiento
});
// A menudo el más rápido para arrays simples
for (const item of arr) {
// procesamiento
}
5. Memoización para las funciones costosas
// Sin memoización
function calculComplexe(n) {
console.log('Calcul pour', n);
// Simulación de un cálculo costoso
return n * n;
}
// Con memoización
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn(...args);
cache.set(key, result);
return result;
};
}
const calculComplexeMemoize = memoize(calculComplexe);
// Primera llamada: cálculo realizado
calculComplexeMemoize(42);
// Segunda llamada con el mismo argumento: resultado almacenado en caché
calculComplexeMemoize(42);
Optimizaciones avanzadas
1. Web Workers para las tareas intensivas
Los Web Workers permiten ejecutar código JavaScript en un hilo separado, evitando bloquear la interfaz de usuario.
// main.js
const worker = new Worker('worker.js');
worker.onmessage = function(e) {
console.log('Résultat reçu du worker:', e.data);
};
worker.postMessage({ data: complexData, operation: 'process' });
// worker.js
self.onmessage = function(e) {
const { data, operation } = e.data;
if (operation === 'process') {
// Procesamiento intensivo que no bloquea la UI
const result = processData(data);
self.postMessage(result);
}
};
2. Virtualización de listas largas
En las listas muy largas, renderiza únicamente los elementos visibles en la ventana de visualización.
// Ejemplo simplificado de virtualización
function renderVisibleItems(items, container, itemHeight) {
const scrollTop = window.scrollY;
const viewportHeight = window.innerHeight;
const startIndex = Math.floor(scrollTop / itemHeight);
const endIndex = Math.min(
items.length - 1,
Math.floor((scrollTop + viewportHeight) / itemHeight)
);
container.style.height = `${items.length * itemHeight}px`;
container.innerHTML = '';
for (let i = startIndex; i <= endIndex; i++) {
const item = document.createElement('div');
item.textContent = items[i];
item.style.position = 'absolute';
item.style.top = `${i * itemHeight}px`;
item.style.height = `${itemHeight}px`;
container.appendChild(item);
}
}
// Uso con un listener de scroll
window.addEventListener('scroll', () => {
renderVisibleItems(myLargeArray, myContainer, 50);
});
3. Uso eficiente de los timers
// Inefectivo: múltiples timers
function inefficientAnimation() {
for (let i = 0; i < 100; i++) {
setTimeout(() => updateElement(i), i * 10);
}
}
// Optimizado: requestAnimationFrame
function efficientAnimation() {
let i = 0;
function step() {
updateElement(i);
i++;
if (i < 100) {
requestAnimationFrame(step);
}
}
requestAnimationFrame(step);
}
4. Debounce y Throttle
// Debounce: ejecuta la función tras un retardo sin llamadas
function debounce(fn, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), delay);
};
}
// Throttle: limita el número de llamadas en el tiempo
function throttle(fn, delay) {
let lastCall = 0;
return function(...args) {
const now = Date.now();
if (now - lastCall >= delay) {
fn.apply(this, args);
lastCall = now;
}
};
}
// Uso
const debouncedResize = debounce(() => {
console.log('Redimensionnement terminé');
}, 200);
const throttledScroll = throttle(() => {
console.log('Défilement contrôlé');
}, 100);
window.addEventListener('resize', debouncedResize);
window.addEventListener('scroll', throttledScroll);
Optimización de la carga
1. Code splitting
Divide tu código en fragmentos más pequeños para cargar solo lo necesario.
// Carga dinámica con import()
button.addEventListener('click', async () => {
const module = await import('./feature.js');
module.default();
});
2. Lazy loading
// Lazy loading de imágenes
document.querySelectorAll('img[data-src]').forEach(img => {
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
img.src = img.dataset.src;
observer.disconnect();
}
});
});
observer.observe(img);
});
// Lazy loading de componentes
const LazyComponent = React.lazy(() => import('./LazyComponent'));
function App() {
return (
<React.Suspense fallback={<div>Chargement...</div>}>
<LazyComponent />
</React.Suspense>
);
}
3. Preload, Prefetch y Preconnect
<!-- Preload: recurso crítico para la página actual -->
<link rel="preload" href="critical.js" as="script">
<!-- Prefetch: recurso para la navegación futura -->
<link rel="prefetch" href="next-page.js" as="script">
<!-- Preconnect: establecer una conexión anticipada -->
<link rel="preconnect" href="https://api.example.com">
Medición y monitorización del rendimiento
1. Performance API
// Medición del tiempo de ejecución
performance.mark('debut');
// Código a medir
performance.mark('fin');
performance.measure('monOperation', 'debut', 'fin');
const mesures = performance.getEntriesByType('measure');
console.log(mesures[0].duration);
// Navigation Timing API
window.addEventListener('load', () => {
const timing = performance.timing;
const pageLoadTime = timing.loadEventEnd - timing.navigationStart;
console.log(`Temps de chargement: ${pageLoadTime}ms`);
});
2. Herramientas de desarrollo
- Chrome DevTools: pestañas Performance, Network y Memory
- Lighthouse: auditoría automatizada del rendimiento
- WebPageTest: pruebas de rendimiento detalladas
- Bundle analyzers: análisis del tamaño de los bundles
Buenas prácticas generales
- Minimizar y comprimir los archivos JavaScript
- Utilizar la caché del navegador de forma eficiente
- Evitar las bibliotecas innecesarias y preferir las soluciones nativas
- Adoptar las funcionalidades modernas de JavaScript (pero comprobar la compatibilidad)
- Hacer profiling con regularidad de tu código para identificar los cuellos de botella
- Optimizar las imágenes y los medios para reducir la carga global
- Utilizar una CDN para los recursos estáticos
- Implementar Server-Side Rendering o generación estática cuando sea posible
Errores comunes que conviene evitar
- Sobreoptimización prematura: mide primero, optimiza después
- Microoptimizaciones excesivas: céntrate en las mejoras significativas
- Ignorar la experiencia móvil: prueba en dispositivos reales y con conexiones lentas
- Descuidar la accesibilidad: las optimizaciones no deben comprometer la accesibilidad
- Olvidar probar tras la optimización: comprueba que todo sigue funcionando correctamente
Conclusión
La optimización del rendimiento de JavaScript es un equilibrio entre velocidad, mantenibilidad y experiencia de usuario. Empieza por las optimizaciones de mayor impacto, mide los resultados y afina progresivamente.
Recuerda que la mejor optimización suele ser el código que no escribes. Antes de añadir una nueva biblioteca o funcionalidad, evalúa su impacto en el rendimiento y si es realmente necesaria.
Siguiendo estas técnicas y buenas prácticas, podrás crear aplicaciones JavaScript rápidas, reactivas y agradables de usar, incluso en dispositivos y conexiones modestos.
Acerca de InSkillCoach
Experto en formación y tecnologías
Coach especializado en tecnologías avanzadas e IA, impulsado por GNeurone Inc.
Certificaciones:
- AWS Certified Solutions Architect – Professional
- Certificaciones de Google Cloud
- Microsoft Certified: DevOps Engineer Expert
- Certified Kubernetes Administrator (CKA)
- CompTIA Security+
Comentarios
Los comentarios funcionan con GitHub Discussions
Inicia sesión con GitHub para participar en la discusión