HDP115

Optimización y Rendimiento en JavaScript

Aprende técnicas avanzadas para mejorar la velocidad y eficiencia de tus aplicaciones JavaScript, desde optimización del DOM hasta estrategias de renderizado.

CE

Cristian Escalante

Última actualización: 24 de abril de 2025

javascript
rendimiento
optimización
desarrollo web

Optimización y Rendimiento

La importancia del rendimiento web

El rendimiento es un factor crítico en el éxito de cualquier aplicación web. Un rendimiento deficiente puede llevar a:

  • Tasas de rebote más altas
  • Menor conversión
  • Experiencia de usuario negativa
  • Peor posicionamiento SEO
  • Mayor consumo de recursos (batería, datos)

Según estudios de Google, el 53% de los usuarios abandonan un sitio si tarda más de 3 segundos en cargar, y cada segundo adicional de carga reduce las conversiones en un 12%.

// El rendimiento no es solo sobre velocidad, sino también sobre percepción
// Técnicas como la carga progresiva pueden mejorar la percepción de velocidad
// aunque el tiempo total de carga sea similar

Métricas de rendimiento (Core Web Vitals)

Las Core Web Vitals son métricas clave definidas por Google que miden la experiencia de usuario en términos de rendimiento.

Largest Contentful Paint (LCP)

Mide el tiempo que tarda en renderizarse el elemento de contenido más grande visible en la ventana. Ideal: < 2.5 segundos.

// Monitorear LCP
new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    console.log('LCP:', entry.startTime);
    console.log('LCP element:', entry.element);
  }
}).observe({ type: 'largest-contentful-paint', buffered: true });

First Input Delay (FID)

Mide el tiempo desde que un usuario interactúa por primera vez con la página hasta que el navegador puede responder a esa interacción. Ideal: < 100 ms.

// Monitorear FID
new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    console.log('FID:', entry.processingStart - entry.startTime);
  }
}).observe({ type: 'first-input', buffered: true });

Cumulative Layout Shift (CLS)

Mide la estabilidad visual de la página, cuantificando cuánto se mueven los elementos durante la carga. Ideal: < 0.1.

// Monitorear CLS
let cls = 0;
new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    if (!entry.hadRecentInput) {
      cls += entry.value;
      console.log('CLS update:', cls);
    }
  }
}).observe({ type: 'layout-shift', buffered: true });

Interaction to Next Paint (INP)

Mide la capacidad de respuesta de la página a las interacciones del usuario. Ideal: < 200 ms.

// Monitorear INP (experimental)
new PerformanceObserver((entryList) => {
  const interactions = entryList.getEntries();
  // Calcular el percentil 95 de los tiempos de interacción
}).observe({ type: 'event', buffered: true });

Herramientas de profiling y detección de cuellos de botella

Chrome DevTools Performance Panel

El panel de rendimiento de Chrome DevTools permite grabar y analizar el rendimiento de la página.

// Marcar eventos en la línea de tiempo para análisis
performance.mark('inicio-operacion');
// ... código a medir ...
performance.mark('fin-operacion');
performance.measure('duracion-operacion', 'inicio-operacion', 'fin-operacion');

Lighthouse

Lighthouse es una herramienta automatizada que audita la calidad de las páginas web, incluyendo rendimiento, accesibilidad y SEO.

Web Vitals Extension

Extensión de Chrome que muestra las métricas Core Web Vitals en tiempo real mientras navegas.

User Timing API

API nativa para medir el rendimiento de operaciones específicas en JavaScript.

// Medir el tiempo de una operación
performance.mark('inicio-calculo');

// Operación costosa
const resultado = calcularDatosComplejos();

performance.mark('fin-calculo');
performance.measure('tiempo-calculo', 'inicio-calculo', 'fin-calculo');

// Obtener y mostrar las mediciones
const mediciones = performance.getEntriesByType('measure');
console.log(`Tiempo de cálculo: ${mediciones[0].duration}ms`);

Optimización del DOM

El DOM (Document Object Model) es una de las partes más costosas en términos de rendimiento en aplicaciones web.

Minimizar manipulaciones del DOM

// Mal (múltiples manipulaciones del DOM)
for (let i = 0; i < 1000; i++) {
  document.getElementById('lista').innerHTML += `<li>Item ${i}</li>`;
}

// Bien (una sola manipulación del DOM)
const items = [];
for (let i = 0; i < 1000; i++) {
  items.push(`<li>Item ${i}</li>`);
}
document.getElementById('lista').innerHTML = items.join('');

Usar DocumentFragment

const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
  const li = document.createElement('li');
  li.textContent = `Item ${i}`;
  fragment.appendChild(li);
}
document.getElementById('lista').appendChild(fragment);

Evitar layout thrashing

El layout thrashing ocurre cuando se alternan lecturas y escrituras del DOM, forzando recálculos de layout.

// Mal (layout thrashing)
for (let i = 0; i < elementos.length; i++) {
  const altura = elementos[i].offsetHeight; // Lectura (fuerza layout)
  elementos[i].style.height = (altura * 2) + 'px'; // Escritura (invalida layout)
}

// Bien (separar lecturas y escrituras)
// Primero, todas las lecturas
const alturas = [];
for (let i = 0; i < elementos.length; i++) {
  alturas.push(elementos[i].offsetHeight);
}
// Después, todas las escrituras
for (let i = 0; i < elementos.length; i++) {
  elementos[i].style.height = (alturas[i] * 2) + 'px';
}

Virtualización de listas

Para listas muy largas, renderizar solo los elementos visibles en la ventana.

// Ejemplo simplificado de virtualización
function renderizarElementosVisibles(items, contenedor, 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)
  );
  
  // Limpiar contenedor
  contenedor.innerHTML = '';
  
  // Añadir espaciador superior
  const topSpacer = document.createElement('div');
  topSpacer.style.height = `${startIndex * itemHeight}px`;
  contenedor.appendChild(topSpacer);
  
  // Renderizar solo elementos visibles
  for (let i = startIndex; i <= endIndex; i++) {
    const item = document.createElement('div');
    item.style.height = `${itemHeight}px`;
    item.textContent = items[i];
    contenedor.appendChild(item);
  }
  
  // Añadir espaciador inferior
  const bottomSpacer = document.createElement('div');
  bottomSpacer.style.height = `${(items.length - endIndex - 1) * itemHeight}px`;
  contenedor.appendChild(bottomSpacer);
}

Lazy loading de recursos

Imágenes y videos

HTML nativo ahora soporta lazy loading para imágenes y videos:

<img src="imagen.jpg" loading="lazy" alt="Descripción" />
<video src="video.mp4" loading="lazy" controls></video>

Implementación manual con Intersection Observer

const observer = new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src; // Cargar la imagen
      observer.unobserve(img); // Dejar de observar
    }
  });
});

// Aplicar a todas las imágenes con data-src
document.querySelectorAll('img[data-src]').forEach(img => {
  observer.observe(img);
});

Carga diferida de JavaScript

<!-- Scripts no críticos con carga diferida -->
<script src="no-critico.js" defer></script>

<!-- O cargar dinámicamente cuando sea necesario -->
<script>
  function cargarScriptCuandoNecesario() {
    const script = document.createElement('script');
    script.src = 'caracteristica-avanzada.js';
    document.body.appendChild(script);
  }
  
  // Cargar solo cuando el usuario interactúa con cierta función
  document.getElementById('boton-caracteristica').addEventListener('click', cargarScriptCuandoNecesario);
</script>

Memoización y caching

Memoización de funciones

La memoización es una técnica que almacena los resultados de operaciones costosas para reutilizarlos cuando se llame a la función con los mismos parámetros.

function memoize(fn) {
  const cache = new Map();
  
  return function(...args) {
    const key = JSON.stringify(args);
    
    if (cache.has(key)) {
      console.log('Usando resultado en caché');
      return cache.get(key);
    }
    
    console.log('Calculando nuevo resultado');
    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

// Ejemplo: Función costosa para calcular fibonacci
function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

// Versión memoizada
const fibonacciMemoizado = memoize(function(n) {
  if (n <= 1) return n;
  return fibonacciMemoizado(n - 1) + fibonacciMemoizado(n - 2);
});

console.time('Sin memoización');
fibonacci(40); // Muy lento
console.timeEnd('Sin memoización');

console.time('Con memoización');
fibonacciMemoizado(40); // Mucho más rápido
console.timeEnd('Con memoización');

Cache API

La Cache API permite almacenar respuestas de red para uso offline y mejora de rendimiento.

// Guardar respuesta en caché
caches.open('v1').then(cache => {
  cache.put('/api/datos', new Response(JSON.stringify({ 
    resultado: 'datos importantes' 
  })));
});

// Recuperar de caché o red
async function obtenerDatos() {
  const cache = await caches.open('v1');
  let respuesta = await cache.match('/api/datos');
  
  if (!respuesta) {
    console.log('Obteniendo datos de la red');
    respuesta = await fetch('/api/datos');
    cache.put('/api/datos', respuesta.clone());
  } else {
    console.log('Usando datos en caché');
  }
  
  return respuesta.json();
}

localStorage y sessionStorage

// Guardar datos
function guardarEnCache(clave, datos, expiracion) {
  const item = {
    valor: datos,
    expira: expiracion ? Date.now() + expiracion : null
  };
  
  localStorage.setItem(clave, JSON.stringify(item));
}

// Recuperar datos
function obtenerDeCache(clave) {
  const itemStr = localStorage.getItem(clave);
  
  if (!itemStr) return null;
  
  const item = JSON.parse(itemStr);
  
  if (item.expira && Date.now() > item.expira) {
    localStorage.removeItem(clave);
    return null;
  }
  
  return item.valor;
}

// Ejemplo de uso
guardarEnCache('usuario', { id: 1, nombre: 'Ana' }, 3600000); // Expira en 1 hora
const usuario = obtenerDeCache('usuario');

Web Workers

Los Web Workers permiten ejecutar código JavaScript en hilos separados del hilo principal, evitando bloquear la interfaz de usuario.

Crear y usar un Web Worker

// main.js (hilo principal)
const worker = new Worker('worker.js');

// Enviar mensaje al worker
worker.postMessage({ 
  accion: 'procesar', 
  datos: [1, 2, 3, 4, 5] 
});

// Recibir resultados del worker
worker.onmessage = function(e) {
  console.log('Resultado del worker:', e.data);
};

// Manejar errores
worker.onerror = function(error) {
  console.error('Error en el worker:', error.message);
};
// worker.js (código ejecutado en un hilo separado)
self.onmessage = function(e) {
  if (e.data.accion === 'procesar') {
    const resultado = procesarDatos(e.data.datos);
    self.postMessage(resultado);
  }
};

function procesarDatos(datos) {
  // Operación costosa que no bloquea la UI
  return datos.map(x => x * x).reduce((a, b) => a + b, 0);
}

Transferencia de datos con Transferable Objects

// Transferir un ArrayBuffer al worker (transferencia de propiedad)
const buffer = new ArrayBuffer(1024 * 1024); // 1MB
worker.postMessage({ buffer }, [buffer]);

SharedArrayBuffer para memoria compartida

// Crear buffer compartido entre hilos
const sharedBuffer = new SharedArrayBuffer(1024);
const view = new Uint8Array(sharedBuffer);

// Enviar al worker
worker.postMessage({ sharedBuffer });

// Uso de Atomics para sincronización
Atomics.store(view, 0, 42);

Service Workers

Los Service Workers actúan como proxies de red, permitiendo controlar cómo se manejan las solicitudes de red para una página.

Registro de un Service Worker

// Registrar service worker
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/sw.js')
      .then(registration => {
        console.log('Service Worker registrado:', registration.scope);
      })
      .catch(error => {
        console.error('Error al registrar Service Worker:', error);
      });
  });
}

Implementación de un Service Worker para caché

// sw.js
const CACHE_NAME = 'mi-sitio-v1';
const URLS_TO_CACHE = [
  '/',
  '/index.html',
  '/styles/main.css',
  '/scripts/main.js',
  '/images/logo.png'
];

// Instalar y precachear recursos
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => {
        console.log('Caché abierta');
        return cache.addAll(URLS_TO_CACHE);
      })
  );
});

// Estrategia de caché: primero caché, luego red
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(response => {
        // Devolver de caché si existe
        if (response) {
          return response;
        }
        
        // Si no, buscar en la red
        return fetch(event.request)
          .then(response => {
            // No cachear respuestas fallidas
            if (!response || response.status !== 200 || response.type !== 'basic') {
              return response;
            }
            
            // Clonar la respuesta para cachearla
            const responseToCache = response.clone();
            caches.open(CACHE_NAME)
              .then(cache => {
                cache.put(event.request, responseToCache);
              });
              
            return response;
          });
      })
  );
});

// Limpiar cachés antiguas
self.addEventListener('activate', event => {
  const cacheWhitelist = [CACHE_NAME];
  
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cacheName => {
          if (cacheWhitelist.indexOf(cacheName) === -1) {
            // Eliminar cachés no incluidas en la whitelist
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

Estrategias de renderizado

Client-Side Rendering (CSR)

El navegador carga un HTML mínimo y JavaScript se encarga de renderizar el contenido.

// Ejemplo con React
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));

Ventajas:

  • Experiencia de usuario fluida después de la carga inicial
  • Menor carga en el servidor

Desventajas:

  • Tiempo hasta contenido interactivo más largo
  • Peor SEO potencialmente
  • Mayor consumo de batería en dispositivos móviles

Server-Side Rendering (SSR)

El servidor genera el HTML completo y lo envía al cliente.

// Ejemplo con Express y React
import express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import App from './App';

const app = express();

app.get('/', (req, res) => {
  const html = renderToString(<App />);
  
  res.send(`
    <!DOCTYPE html>
    <html>
      <head>
        <title>Mi App</title>
      </head>
      <body>
        <div id="root">${html}</div>
        <script src="/bundle.js"></script>
      </body>
    </html>
  `);
});

app.listen(3000);

Ventajas:

  • Mejor First Contentful Paint
  • Mejor SEO
  • Funciona sin JavaScript

Desventajas:

  • Mayor carga en el servidor
  • Navegación entre páginas menos fluida

Static Site Generation (SSG)

Generar HTML estático en tiempo de compilación.

// Ejemplo con Next.js
export async function getStaticProps() {
  const res = await fetch('https://api.example.com/data');
  const data = await res.json();
  
  return {
    props: { data },
    // Regenerar la página cada hora
    revalidate: 3600
  };
}

function Page({ data }) {
  return <div>{data.title}</div>;
}

export default Page;

Ventajas:

  • Rendimiento óptimo
  • Seguridad mejorada
  • Menor costo de hosting

Desventajas:

  • No adecuado para contenido muy dinámico
  • Proceso de compilación más largo

Incremental Static Regeneration (ISR)

Regenerar páginas estáticas bajo demanda después de un tiempo determinado.

// Ejemplo con Next.js
export async function getStaticProps() {
  const res = await fetch('https://api.example.com/data');
  const data = await res.json();
  
  return {
    props: { data },
    // Regenerar la página cada hora
    revalidate: 3600
  };
}

export async function getStaticPaths() {
  return {
    paths: [
      { params: { id: '1' } },
      { params: { id: '2' } }
    ],
    // Generar otras páginas bajo demanda
    fallback: 'blocking'
  };
}

Optimización de JavaScript

Code splitting

Dividir el código en chunks más pequeños que se cargan bajo demanda.

// Importación dinámica con webpack
button.addEventListener('click', async () => {
  const { mostrarModal } = await import('./modal.js');
  mostrarModal();
});

Tree shaking

Eliminar código no utilizado durante el proceso de bundling.

// Importar solo lo necesario
import { funcion1 } from './utilidades';
// En lugar de:
// import * from './utilidades';

Minificación y compresión

// Configuración de webpack para producción
module.exports = {
  mode: 'production', // Habilita minificación
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: {
            drop_console: true, // Eliminar console.log
          }
        }
      })
    ]
  }
};

Conclusión

La optimización del rendimiento en JavaScript es un proceso continuo que requiere medición, análisis y mejora constante. Las técnicas presentadas en este artículo pueden ayudar significativamente a mejorar la velocidad y eficiencia de tus aplicaciones web, pero es importante recordar que:

  1. Medir antes de optimizar: Usa herramientas de profiling para identificar los verdaderos cuellos de botella.
  2. Enfocarse en la percepción del usuario: A veces, mejorar la percepción de velocidad es tan importante como la velocidad real.
  3. Balancear rendimiento y mantenibilidad: El código más rápido no siempre es el más legible o mantenible.
  4. Optimizar para dispositivos móviles: Los dispositivos móviles tienen restricciones de CPU, memoria y batería más severas.

Implementar estas técnicas te permitirá crear aplicaciones web más rápidas, eficientes y con mejor experiencia de usuario, lo que se traduce directamente en mejores métricas de negocio y satisfacción del usuario.

Testing en JavaScript
Aprende a implementar pruebas efectivas para garantizar la c...
Referencias
Google Developers. Web Vitals. https://web.dev/vitals/
Ilya Grigorik. High Performance Browser Networking. https://hpbn.co/

Conceptos Básicos de HTML

Aprende los conceptos básicos de HTML

Conceptos Básicos de CSS

Aprende los conceptos básicos de CSS

Conceptos Básicos SQL

Aprende los conceptos básicos de SQL

Conceptos Básicos de GIT

Aprende los conceptos básicos de GIT

Conceptos Básicos de Python

Aprende los conceptos básicos de Python

Conceptos Básicos de UML

Aprende los conceptos básicos de UML

Refuerzo Academico de Herramientas de Productividad 2025