Fase 4: Rendimiento web (PerforMANZ)

Contenidos

  • Dev Tools
  • Optimización de código
  • Minificación de recursos
  • Testing

¿Qué es la optimización?

  • Un sistema debe funcionar. Es el primer paso.
  • Hay varios niveles a los que puede funcionar: 🟥 mal, 🟧 regular, 🟨 normal, 🟩 muy bien, 🟩🟩 excelente...
  • Normalmente esto no preocupa... hasta que falla y se vuelve un problema grave.
  • No siempre hay que aplicarlo. Pero anticiparse suele ser una buena forma de prevenir problemas graves.

Optimización

  • Memoria: Cuanta menos RAM se use, mejor. Evitar páginas lentas o «congelamiento».
  • Tiempo: Reducir el tiempo que tarda en realizar tareas. Evitar páginas lentas.
  • Transferencia: Reducir el tiempo que tarda en descargar recursos. Evitar páginas lentas.
  • Paquetes: Reducir la cantidad de dependencias que usamos. Más código que procesar, más lentitud.
  • Medición: Sin datos, no sabes que optimizar, donde está fallando o si lo que cambias está funcionando.

Resumen: Evitar páginas lentas.

Chrome Dev Tools

Dev Tools

Puntos clave

  • Inspección del DOM en tiempo real
  • Icono flecha: Seleccionar un elemento
  • Icono devices: Modo responsive
  • Elements: Ver o editar el HTML
  • Styles: Ver o editar el CSS
    • Forzar :hover, :focus, :active
    • Widgets interactivos
    • Computed: CSS computado
  • Console: Abrir consola de Javascript

Network Dev Tools

Network Tools

  • Nombre de ficheros
  • Método (GET, POST, HEAD...)
  • Estado HTTP → HTTP.Cat
  • Protocolo → HTTP1.1 < h2 < h3
  • Tipo y donde se inicia la petición
  • Tamaño del fichero / cacheado
  • Tiempo de descarga + Prioridad
  • Abajo: Peticiones, tamaño y tiempos
  • Disable cache / Throttling

Uso de memoria

  • Garbage Collector
  • Memory leaks
  • Listeners huérfanos
  • Windowing

Garbage Collector

  • Es un proceso automático del lenguaje que libera memoria
const data = [/* Array con muchos datos */];

function method() {
  /* ... */
}

// ANTES: Memoria ocupada -→ 🟩🟩🟥🟥
method(); // Memoria ocupada 🟩🟩🟥🟥
// LUEGO: Memoria ocupada -→ 🟩🟩🟥🟥

// data es global, ocupa durante todo el programa
function method() {
  const data = [/* Array con muchos datos */];
  /* ... */
}

// ANTES: Memoria libre ---→ 🟩🟩🟩🟩
method(); // Memoria ocupada 🟩🟩🟥🟥
/* Si no quedan referencias a data */
// LUEGO: Memoria libre ---→ 🟩🟩🟩🟩

// data es local, ocupa sólo durante función
  • El GC decide cuando liberar memoria (no lo decides tú)
  • El GC elimina cuando no son alcanzables (sin referencias)

Recuerda:

  • const a = b → Esto es una referencia, no una copia

  • El objeto nace
  • Si el objeto no tiene ninguna referencia a él...
    • El motor lo marca como inalcanzables (0 ref)
    • El Garbage Collector los recolecta para borrarlos

Memory leaks

  • Si existen referencias a un objeto que ya no necesitas, GC no puede liberar su memoria
  • Esa memoria no podrá reutilizarse hasta cerrar la aplicación o web (que se liberan sus recursos)
// main.js
import { moduleVar } from "./module.js";

const global = []; // ❌ Global: siempre viva

function method() {
  const local = [];   // ✅ Local: Se limpia al terminar fx
}

// module.js
const moduleVar = []; // ❌ Global: Siempre viva
window.noRemove = []; // ❌ Global: Siempre viva
globalThis.same = []; // ❌ Global: Siempre viva

1️⃣ Variables globales / de módulo → 💧 leak

  • ❌ Evita variables globales muy grandes
  • ❌ Evita meter vars en window o globalThis
  • ⚠️ Variables de módulos ESM → son singletons
    • Se cargan una vez y viven toda la sesión

  • 🗑 Cualquier objeto asignado así nunca será recolectado
const cache = new Map();
cache.set(id, value); // LEAK: Nunca se elimina ❌

// LRU: Max. 500 (más antiguo sale) ✅
const cache = new QuickLRU({ maxSize: 500 });
cache.set(id, value);

// TTL: Expira por tiempo de vida ✅
const cache = new Map();
cache.set(id, { value, expires: Date.now() + 60_000 });
// Añadir lógica para comprobar TTL y borrar

// WeakMap: Autoborrado si "domNode" muere ✅
const cache = new WeakMap();
cache.set(domNode, value);

2️⃣ Caché sin límite → 💧 leak

  • Un Map o Array que no se limita → leak (silencioso)
  • No da error, simplemente consume más y más
  • ✅ Un caché debería tener límite

Soluciones:

  • LRULeast Recently Usedlru-cache o quick-lru
  • TTL → Revisa si supera tiempo de vida ttlcache o tiny-lru
  • WeakMap → Si key es objeto (DOM, clase...), la entrada desaparece cuando es recolectado
// ❌ Leak clásico
let btn = document.querySelector(".button");  // global
btn.remove();
// Se elimina el .button del DOM (HTML)
// "btn" sigue referenciado por la variable global
// ❌ El GC no puede recolectarlo

// ✅ Correcto
{
  let btn = document.querySelector(".button");  // local
  btn.remove();
}
btn = null  // Si es global, fuerza a eliminar la referencia
// ✅ Ahora el GC puede recolectarlo

3️⃣ Detached node → 💧 leak

  • Detached: Nodos eliminados pero aún referenciados
    • ⚠️ Sigue referenciado → GC no puede recolectarlo
    • ❌ Puede retener nodos hijos, listeners...

  • Cómo buscarlos:
    • Dev Tools / Memory / Buscar "Detached"
    • Variables globales o de módulo
    • Stores globales (montar componentes sin desmontarlos)
    • Nodos globales en array "para procesar luego"

Listeners huérfanos

  • Un listener huérfano → Continua escuchando eventos desde un elemento que se ha eliminado del DOM
  • Como no se eliminó el evento, sigue escuchando y el GC no puede recolectarlo
const mountModal() {
  const btn = document.querySelector(".close");
  /* ... */

  document.addEventListener("keydown", ev => {
    if (e.key === "Escape") closeModal();
  });
}




function mountModal() {
  /* ... */

  const controller = new AbortController();
  const { signal } = controller;

  document.addEventListener("keydown", () => { ... }, { signal })
  document.addEventListener("click", () => { ... }, { signal })
  globalThis.addEventListener("resize", () => { ... }, { signal })

  return () => controller.abort() // elimina los tres de golpe
}
// ❌ Se añade un listener por botón
const buttons = document.querySelectorAll(".button");
buttons.forEach(btn =>
  btn.addEventListener("click", () => { ... })
);

// ✅ Un listener padre para gobernarlos a todos 💍
const parent = document.querySelector(".parent");
parent.addEventListener("click", ev => {
  const btn = ev.target.closest(".button");
  if (btn) () => { ... }
});
  • 👁‍🗨 Cada vez que se monta, se añade listener nuevo
  • 🔥 Si se monta/desmonta 10 veces → 10 listeners

Soluciones:

  • 1️⃣ ¿Puedes resolverlo sin Javascript? → <dialog>
  • 2️⃣ Añade un removeEventListener
  • 3️⃣ Usa AbortController para cancelar todos
  • 4️⃣ Usa delegación de eventos en el padre

Windowing: Listas virtuales

  • 📋 Tenemos una web con una tabla con MUCHOS datos/elementos (nodos) → Windowing

El problema

  • 🧠 Aparentemente simple, pero problema complejo.
  • Renderizar y mantener MUCHOS nodos vivos es costoso
  • Cada elemento requiere tareas de renderización
  • AL FINAL: El usuario sólo ve ~10-15 nodos
  • ¿Cuando optimizar?

    • ☁️ < 100 nodos✅ No hace falta optimizar
    • ⚖️ 100-500 nodos❌ Si son complejos, optimizar
    • 🧱 > 500 nodos❌ Optimizar siempre

Soluciones

  • Sólo renderizar los nodos en pantalla
  • Añadir unos nodos «offset» (para evitar FOUC/saltos)
  • JS: IntersectionObserver
    • ❌ Rompe posición scroll, sólo es lazy rendering
  • CSS: content-visibility
    • ❌ Ahorra rendering, pero los nodos siguen en memoria
  • Librerías:

Uso de tiempo (Eficiencia)

  • Memoización
  • Frecuencia de ejecución
  • Hilo principal
  • Sincronización de rendering
  • Renderización de layout

Complejidad algorítmica (Big O notation)

  • Describe como crece el tiempo de ejecución (o uso de memoria) del código en función del tamaño de la entrada.
// ✅ O(1) - Acceso directo (rápido)
// No se recorre ninguna estructura
const val = map.get(key);
const item = array[index];

// ✅ O(log n) - Búsqueda binaria, árbol
// Se recorre una estructura (no completamente)
array.sort();

// 🟨 O(n) - Se recorre una vez (completamente)
array.find(x => x.id === id);
array.filter(x => x.active);
// 🟥 O(n²) - Bucle dentro de bucle (peligro)
const duplicates = items.filter(item => isDuplicate(item));
const isDuplicate = (item) =>
  items.filter(other => other.id === item.id).length > 1;

// ✅ Se optimiza con Set → O(n) → Elimina recorrido interior
const seen = new Set()
const duplicates = items.filter(item => {
  if (seen.has(item.id)) return true
  seen.add(item.id)
  return false
})

Memoización

  • No hacer trabajo que ya hiciste (o que no necesitas hacer).
function memoize(fn) {
  const cache = new Map();

  return function(...args) {
    const key = JSON.stringify(args); // Puede ser lento

    if (cache.has(key)) return cache.get(key);

    const result = fn(...args);
    cache.set(key, result);
    return result;
  };
}

const expensiveCalc = memoize((x, y) => { ... });

1️⃣ Memoización manual

  • Básicamente: cachear entrada-salida
  • La función debe ser pura → no alterar el exterior

  • Usamos un Map como caché
  • Si existe en caché, devolvemos el cacheado
  • Si no existe en caché, ejecutamos y guardamos en caché

  • ⚠️ Cuidado: En este ejemplo...
    • ...entradas grandes son lentas (JSON.stringify())
class Movie {
  constructor(title, ratings) {
    this.title = title;
    this.ratings = ratings; // [7, 8, 6, 9, 8]
  }

  get averageRating() {  // 🟥 Costoso → Guardar
    const total = this.ratings.reduce(...);
    return total / this.ratings.length;
  }
}

const movie = new Movie("Origen", [7, 8, 6, 9, 8]);
movie.averageRating;  // Calcula
movie.averageRating;  // ❌ Vuelve a calcular

2️⃣ Lazy evaluation

  • No calcular un valor hasta que se necesita por primera vez.
  • El patrón más limpio en JS es usar un getter

  • El getter averageRating es una tarea costosa
  • Se ejecuta la primera vez ✅
  • La idea es que si ya se ejecutó antes, no se calcule de nuevo

  • Solución: Simplemente guardar en una propiedad privada
  • Si existe esa propiedad, se devuelve (no se ejecuta calculos)
const name = user.name || computeDefaultName();
// Si user.name existe, el de derecha no se llama

isAuthenticated && loadUserData();
// derecha solo se llama si isAuthenticated es true

// nullish coalescing (??)
const timeout = config.timeout ?? getDefaultTimeout();

// Pon primero la condición más barata/frecuente
if (isSimpleCase(x) || expensiveCheck(x)) { ... }

3️⃣ Short-circuit

  • JavaScript evalúa && y || de izquierda a derecha
  • Se detiene si ya conoce el resultado (cortocircuito)
  • La expresión de la derecha no se evalúa si no hace falta

  • "" || "Unknown""Unknown" → ("" es falsy)
  • "" ?? "Unknown"0 → ("" es un valor aceptable)
  • true && console.log("Ey!")"Ey!"
  • false && console.log("Ey!")false

Frecuencia de ejecución

  • Limitar la frecuencia de ejecución de una función (en un tiempo concreto)
function debounce(fn, ms) {
  let timer;

  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), ms);
  }
}

const onSearch = debounce(fetchResults, 300);
input.addEventListener("input", onSearch);

1️⃣ Debounce:

  • Ejecuta solo cuando han pasado N ms sin que se llame.
  • Cada llamada reinicia el timer.
  • Se suele usar para evitar llamadas excesivas al backend.
function throttle(fn, ms) {
  let last = 0;

  return function(...args) {
    const now = Date.now();
    if (now - last >= ms) {
      last = now;
      fn(...args);
    }
  }
}

const onScroll = throttle(updateHeader, 100);
globalThis.addEventListener("scroll", onScroll);

2️⃣ Throttle:

  • Ejecuta la función máximo una vez cada N ms.
  • No importa cuántas veces se llame.
  • El resto se ignoran.

Hilo principal

  • Javascript se ejecuta en un thread (comparte tiempo de CPU con código + layout + paint + user events).
  • Cualquier tarea que tarde más de ~16ms (60 fps) puede producir congelamiento o saltos visibles.
// main.js
const heavyArray = new Array(1_000_000).fill(1.5); // ~40MB
const worker = new Worker("worker.js");
worker.postMessage(heavyArray);

worker.onmessage = (ev) => {
  console.log("resultado:", ev.data);
}

// worker.js
self.onmessage = (ev) => {
  // No bloquea UI
  const result = ev.data.map(n => n * 2); // No bloquea
  self.postMessage(result);
}

1️⃣ Web Workers

  • Es un archivo .js que corre en un hilo separado...
    • ...sin acceso al DOM
    • ...sin bloquear el UI

  • En este ejemplo:
    • ❌ Sin worker, el .map() 🥶 congelaría el hilo principal
    • Con datos grandes es un problema 👎
    • postMessage() usa structuredClone()
      • Existen DOS copias en memoria 🟥🟥⬛⬛ ~16MB
const buffer = new Float64Array(1_000_000).fill(1.5); // ~8MB

// ❌ Copia 16MB, lento
// Parámetro 1: Lo que se va a enviar
worker.postMessage({ buffer });

// ✅ Transfiere O(1), no copia, solo cambia quien posee datos
// Parámetro 2: Lista de lo que se va a transferir
worker.postMessage({ buffer }, [buffer]);

2️⃣ Transferables

  • El problema de postMessage(): la copia
  • Por defecto, usa structuredClone() y copia todos los datos

  • En lugar de copiar, transferimos el buffer al worker
  • El hilo principal pierde el acceso, pero el proceso es inmediato
  • El buffer ahora pertenece al worker

Sincronización de rendering

  • Programar «operaciones futuras»: ejecutar funciones en un momento específico del futuro.
const update = (x) => (element.style.left = `${x++}px`);

// Ojo: Ningún timer es exacto (pueden retrasarse)
setTimeout(() => update(x), 16);  // Tras ~16ms
setTimeout(() => update(x), 0);   // Tras ~1-4ms
setInterval(() => update(x), 16); // Tras ~16ms

function animate() {
  update(x);
  requestAnimationFrame(animate);
}

requestAnimationFrame(animate);   // Tras 16ms (sync)

1️⃣ setTimeout (⏱️ Ejecuta después de un delay)

  • ⚠️ setTimeout(fn, 0) no es inmediato, puede retrasarse

2️⃣ setInterval (⏱️ Ejecuta cada N ms)

  • ❌ No sincronizada con el render
  • ❌ Puede acumularse si el frame tardó más de 16ms
  • ❌ Sigue ejecutando aunque la pestaña esté inactiva

3️⃣ rAF (⏱️ justo antes de pintar frame)

  • ✅ Sincronizada con el vsync del monitor
  • ✅ Se pausa automáticamente en pestaña inactiva
  • ⚠️ Si no necesitas lógica por frame → .animate() (rAF)

Renderizado y layout

  • El navegador renderiza en varias fases → ¿Qué fases? Entiéndelo para evitar trabajo innecesario → CSS Triggers
.list-item {
  /* El layout exterior no cambia, solo recalcula interior */
  contain: layout;
  /* Navegador puede ignorar elemento si está fuera del viewport */
  contain: paint;
  /* No depende del contenido (necesita tamaño explícito) */
  contain: size;
  /* Equivalente a activar los 3 anteriores a la vez */
  contain: strict;
}

1️⃣ Javascript

  • Scripts, eventos, timers (motor de JS)

2️⃣ Estilos

  • Calcular qué reglas CSS aplican a cada elemento

3️⃣ Layout

  • Calcular posición y tamaño de cada elemento

4️⃣ Paint

  • Dibujar píxeles: colores, bordes, sombras

5️⃣ Composite (capas)

  • Combinar capas y enviar a monitor vía GPU

Uso de ancho de banda (Transferencia)

  • Resource Hints
  • Lazy loading
  • Tree shaking
  • Sistemas de caché

Resource Hints

  • Etiquetas <link> que indican recursos que vas a necesitar
  • Con ellas preparas al navegador antes de se soliciten y tengas que esperar a que se descarguen.
<!-- Mejora conexión a otros dominios -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://api.dominio.com">
<!-- Úsalo para APIs, CDNs de fuentes, analytics -->
<!-- Máx: 4-6 (cada conexión consume recursos) -->

<!-- Precargar script que va a ser usado (baja prioridad) -->
<link rel="prefetch" href="/chunks/dashboard.js" as="script">

1️⃣ preconnect

  • Realiza Búsqueda DNS + Negocicación TCP/TLS
    • Cuando: Antes de que se solicite el primer recurso
  • 💡 Ahorras 100–500ms en la primera petición a ese origen

2️⃣ prefetch

  • Descarga un recurso con baja prioridad
    • Cuando: Descarga en idle + lo guarda en caché
  • 💡 Úsalo si lo necesitas en la sig. navegación (no en la actual)
<!-- Fuente crítica (dales siempre prioridad) -->
<link rel="preload" href="/fonts/outfit.woff2" as="font"
      type="font/woff2" crossorigin>

<!-- Imagen principal (en primer impacto visual) -->
<link rel="preload" href="/hero.webp" as="image">

<!-- Algo que sabes que se necesitará inmediatamente -->
<link rel="preload" href="/critical.js" as="script">

<!-- ⚠ Si precargas algo y no lo usas en 3s, warning -->

3️⃣ preload

  • Descarga un recurso lo antes posible (precarga)
  • Cuando: Descarga inmediatamente, ejecuta cuando lo necesite
  • 💡 Sólo si lo necesitas inmediatamente
<!-- Precarga el módulo y sus dependencias -->
<link rel="modulepreload" href="/js/app.js">
<link rel="modulepreload" href="/modules/vendor-react.js">
<link rel="modulepreload" href="/modules/router.js">

4️⃣ modulepreload

  • Como preload pero para módulos (ESM).
    • Descarga, parsea y procesa el módulo
    • Crea grafo de dependencias (evita "efecto cascada")
  • 💡 Efecto cascada → Cada import "descubre" el siguiente

Lazy loading

  • Técnica que no carga un recurso hasta que se necesita
  • ...pero "necesita" puede tener significados distintos según el caso
<!-- Carga la imagen al procesar este HTML -->
<img loading="eager" src="foto.jpg" alt="...">

<!-- Precarga. Dentro del <head> -->
<link rel="preload" href="/img/foto.jpg" as="image">
<link rel="preconnect" href="https://cdn.manz.dev/">
<link rel="dns-prefetch" href="https://cdn.manz.dev/">

<!-- Bloquea render. Dentro del <head> -->
<link rel="expect" href="#nombre" blocking="render">

1️⃣ Eager (Acceso crítico)

  • El atributo loading="eager" indica que realice la petición
  • Es el valor por defecto

  • El preload da prioridad absoluta💡 Tipos fonts
  • El dns-prefetch precarga dominiossolo dns
  • El preconnectdns + tcp + tls (lo vimos antes)

  • ⚠ El expect bloquea render hasta que un id se haya cargado
<!-- Imágenes: Buen soporte -->
<img loading="lazy" src="foto.jpg" alt="...">

<!-- Iframe: Buen soporte -->
<iframe loading="lazy" src="https://manz.dev/"></iframe>

<!-- Video/Audio: Soporte reducido aún -->
<video loading="lazy" src="video.mp4"></video>
<audio loading="lazy" src="audio.mp3"></audio>

2️⃣ Lazy por viewport

  • El atributo loading="lazy" pospone carga fuera del viewport
  • El atributo width y height son obligatorios con lazy loading
    • Sin tamaño, navegador no sabe calcular y produce CLS

  • No uses loading=lazy en elementos por encima del «FOLD»
<!-- Prefetch. Dentro del <head> -->
<link rel="prefetch" href="/img/foto.jpg" as="image">

 

// Carga cuando el hilo está libre
requestIdleCallback(() => import(`/pages/dashboard.js`));

3️⃣ Lazy por idle

  • El prefetch carga recurso cuando está en idle (lo vimos antes)
  • El requestIdleCallback() se llama cuando tiene tiempo libre
  • En la función de callback se pueden realizar acciones como:
    • Añadir <link> al document.head
    • Importar ficheros o recursos con import()
// Añade el <link rel="modulepreload"> cuando "hover"
button.addEventListener("mouseenter", () => {
  const link = document.createElement("link");
  link.rel = "modulepreload";
  link.href = `/pages/dashboard.js`;
  document.head.append(link);
}, { once: true });

// Se hace "click", importa recurso y ejecuta render()
button.addEventListener("click", async (ev) => {
  ev.preventDefault();
  const { render } = await import("/pages/dashboard.js");
  render();
});

4️⃣ Lazy por interacción

  • Al hacer hover, se añade el resource hint
    • El modulepreload → Descarga + parsea módulo .js
    • Procesa las dependencias (import)
    • Lo deja listo para ejecutar

  • Cuando se hace click...
    • Se invalida la acción por defecto
    • Se importa el render ya descargado y parseado
    • Se ejecuta render()

Tree shaking

  • El bundler (vite, webpack...) es capaz de eliminar código que se importa pero nunca se usa.
    El nombre viene de "sacudir el árbol" para que caigan las hojas muertas
// ✅ Usar ESM (necesario para saber que se usa)
import { format } from "date-fns";
import * as utils from "./utils.js";

// Si importas todo, no uses keys
utils.format();    // ✅ Hace tree-shaking
utils["format"](); // ❌ No tree-shakeable

// ❌ No usar CommonJS (no puede analizar)
const { format } = require("date-fns");

1️⃣ Tree Shaking

  • ✅ Requisito fundamental: ESM puro → import / export
  • ✅ Preferir import nombrados selectivos con { y }
  • ❌ No usar keys si importas todo el módulo
  • ❌ No sirve con CommonJS → require() / module.exports
// ❌ index.js re-exporta todo
export * from "./Button";      // Puede ser un archivo
export * from "./components";  // Es una carpeta
export * from "./Table";       // Pesa 50KB
// Bundler ve `export *` → incluye todo "por si acaso"

// ✅ Importa directamente del archivo fuente
import { Button } from "./components";        // ❌ No
import { Button } from "./components/Button"; // ✅

// ✅ O usa re-exports nombrados (sin comodines)
export * from "./Modal";            // ❌ No
export { Modal } from "./Modal";    // ✅

2️⃣ Evitar barrels

  • Barrel: Entrypoint único que re-exporta todo desde otro lugar
  • ❌ Evita re-exportaciones (o hazlas con cuidado)
  • ❌ Un problema grave → re-exportar carpetas (común en TS)
  • ✅ Si no usas bundler, no hay problema de tamaño del bundle
  • ✅ Si usas barrels → directos y sin wildcards

  • Más información sobre los Barrels

Sistemas de caché

  • El caché es un almacenamiento temporal para evitar repetir acciones y hacerlas más rápido → Cache
🅰 Sin caché (primera visita)
💻 Browser +-→ /index.html ←→ 🌍 Internet ←→ 💻 Server
               /index.css  ←→ 🌍 Internet ←→ 💻 Server
               /index.js   ←→ 🌍 Internet ←→ 💻 Server
               /logo.webp  ←→ 🌍 Internet ←→ 💻 Server

🅱 Con caché (segunda visita)
💻 Browser +-→ /page2.html ←→ 🌍 Internet ←→ 💻 Server
               /index.css  ☑ Cacheado. Ya lo tiene.
               /index.js   ☑ Cacheado. Ya lo tiene.
               /logo.webp  ☑ Cacheado. Ya lo tiene.

1️⃣ Cachea el «pasado»

  • Aquí hablamos de caché en el navegador

  • 🅰 Descargas sin caché de navegador
  • 🅱 Descargas con caché de navegador
[⚡] Caché a nivel de navegador
💻 Browser [⚡] → 🌍 Internet → 💻 Server

[⚡] Caché a nivel de red (Internet)
💻 Browser → 🌍 Internet [⚡] → 💻 Server

[⚡] Caché a nivel de servidor
💻 Browser → 🌍 Internet → 💻 Server [⚡]

2️⃣ Niveles de caché

  • 1️⃣ Caché de navegador
    index.js

  • 2️⃣ Caché de red
    🛡 Uso de CDN (Ej: Cloudflare)
    Distribuidos geográficamente
    index.a73b2f.js (Útil para evitar caché red)

  • 3️⃣ Caché de servidor
    → Guarda copias, evita operaciones costosas
    → Bases de datos, calculos...
    → Ejemplos: Redis, caché SQL, etc...

HTTP/1.1:  GET /1 → ⌛🟦 → GET /2 → ⌛🟦 → ...

HTTP/2:    GET /1 ┐          ┌ 🟦
           GET /2 ├─ misma ──┤ 🟦  (todo a la vez)
           GET /3 ┘ conexión └ 🟦

HTTP/2 sobre TCP:
  stream A ──────────❌ (pérdida) ➡ todos ⌛
  stream B ──────────⏸ ⌛
  stream C ──────────⏸ ⌛

HTTP/3 sobre QUIC:
  stream A ──────────❌ (pérdida) ➡ solo A ⌛
  stream B ────────────────────── ✅ sigue
  stream C ────────────────────── ✅ sigue

3️⃣ Protocolos de red

  • 1️⃣ HTTP/1.1
    ❌ Sólo una petición TCP a la vez
    🟧 Navegadores usan 6 peticiones paralelas
    🔥 Técnicas agresivas (todo en un bundle) 🆘

  • 2️⃣ HTTP/2
    ✅ Multiplexing: Múltiples peticiones
    ✅ No es necesario bundling agresivo

  • 3️⃣ HTTP/3
    ✅ Reemplaza TCP por QUIC/UDP
    ✅ Mejor rendimiento si se pierden paquetes

<script type="speculationrules">
{
  "prefetch": [{
    "urls": ["/productos", "/sobre-nosotros"]
  }],
  "prerender": [{
    "where": { "href_matches": "/productos/*" },
    "eagerness": "moderate"
  }]
}
</script>

3️⃣ Cachear el «futuro»

  • 🆕 Speculation Rules: API moderna
  • Cachea/renderiza anticipadamente
  • prefetch → Sólo descarga HTML (coste bajo)
  • prerender → Descarga + Ejecuta JS + Renderiza (coste muy alto)
  • eagerness → Controla CUANDO comienza:
    • 🟥 eager (urgente, desde parsear la regla)
    • 🟨 moderate (cursor se acerca a enlace)
    • 🟩 conservative (empieza a hacer clic)

Uso de paquetes NPM

  • Evaluar antes de instalar
  • Analizando bundle
  • Limitar tamaño (CI)
  • Dependencias no usadas

Análisis de paquetes NPM

🟨 Coste de dependencias

  • ✅ Hacen que sea más fácil y rápido desarrollar
  • ⚠️ Tienen un coste:
    • 1️⃣ Tamaño → Debe descargarse
    • 2️⃣ Parseo → Debe leerse y procesarse
    • 3️⃣ Ejecución → Debe ejecutarse y actuar
  • ⚠️ Tienen un riesgo:
    • 1️⃣ Mantenimiento → Debes mantenerla actualizada
    • 2️⃣ Seguridad → Es un posible vector de ataque
    • 3️⃣ Futuro → Debes adaptarte a su evolución
  • 💔 Cada dependencia tiene más dependencias

🟥 ¿Qué podemos hacer?

  • 🔥 Vigila bien si son imprescindibles
    • Ten siempre alternativas presentes y viables.
    • ¿Tiene alternativa nativa?
    • ¿Se puede reutilizar otra dependencia?
    • ¿Se puede buscar alternativas más ligeras?

  • 📦 Detalles de las dependencias: npmjs vs npmx
  • 👀 Evaluar antes de instalar

Bundle (Todo el JS unido en un archivo final)

Bundle


Minificación de recursos

  • Imágenes
  • Multimedia
  • Código (HTML/CSS/JS)

Optimización de imágenes

sudo apt install imagemagick gmic
pnpm install -g sharp

sudo apt install optipng
cargo install oxipng

sudo apt install libjxl-tools
sudo apt install libavif-bin

sudo apt install svgo
cargo install oxvg
# Imagemagick / Sharp
convert input.png output.webp
sharp -i input.png -o output.webp

# Optimizadores
oxipng image.png
cjxl input.png output.jxl
avifenc input.png output.avif

# SVG
svgo input.svg -o output.svg
oxvg optimise input.svg -o output.svg

Optimización multimedia

# Mostrar codecs concretos
ffmpeg -codecs

# Convertir de un formato a otro
ffmpeg -i input.mp4 output.webm

# Convertir usando codecs concretos
ffmpeg -i input.mkv -vcodec libx264 output.mp4
ffmpeg -i input.mkv -vcodec libx265 output.mp4
# Mediante ghostscript
sudo apt install ghostscript
gs -sDEVICE=pdfwrite -dCompatibilityLevel=1.4 \
   -dPDFSETTINGS=/ebook \
   -dNOPAUSE -dQUIET -dBATCH \
   -sOutputFile=output.pdf input.pdf

# Mediante qpdf
sudo apt install qpdf
qpdf --linearize --optimize-images \
   input.pdf output.pdf
  • Conversor multimedia: ffmpeg
  • Sirve para video: .mp4 (H.264), .webm (VP8/VP9/AV1)
  • Sirve para audio: .mp3, .aac, .ogg, .opus, .flac o .wav
  • Tutorial completo sobre ffmpeg

  • Optimizar ficheros PDF con GhostScript
  • Calidades: /screen < /ebook < /printer
  • Optimizar ficheros PDF con qpdf

Optimización HTML/CSS/JS

It's safe

↑ It's safe

Medición y análisis (Monitorización)

  • Testing
  • Core Web Vitals
  • Lighthouse

Vitest (Testing)

  • 🏆 Importancia del testing:
    • «¿Cómo sabes que tu código funciona?» Probando manualmente
    • «¿Cómo sabes que un cambio no va a romperlo?» No lo sabes
    • Tener tests no garantiza que no hayan bugs

  • 🌀 Tipos de testing
    • Estático: Errores en editor (antes) → Typescript o ESLint
    • Unitarios: Aislados (sin dependencias) → function o class
    • Integración: Como colaboran varias piezas → Componente ► DOM
    • End-to-end (e2e): Simulas usuario real del navegador → Playwright

Primeros pasos

  • Jest: framework de tests para Node.
  • Vitest: Jest para Vite, más moderno, rápido y actualizado.
pnpm install -D vite vitest happy-dom
import { defineConfig } from "vite";

export default defineConfig({
  root: "./src",
  test: {
    environment: "happy-dom",
    globals: true
  }
});

Tests de ejemplo

  • Desde una terminal:

  • Desde VSCode:

Creando tests unitarios

  • API básica: describe, it, test, expect y matchers toBe, toEqual...
// Fichero add.js
export function add(a, b) {
  return a + b
}

/**
 *
 * add(1, 2) devuelve 3
 *
 * add(-1, -1) devuelve -2
 *
 * add(5, 0) devuelve 5
 *
 * add(2) devuelve
 *
 */
// Fichero add.test.js
import { describe, it, test, expect } from "vitest";

describe("add", () => {
  it("suma dos números positivos", () => {
    expect(add(1, 2)).toBe(3)                // Si sumas 1 + 2, esperas 3
  })

  it("suma números negativos", () => {
    expect(add(-1, -2)).toBe(-3)             // Si sumas -1 + -2, esperas -3
  })

  test("suma cero", () => {                  // it() y test() son idénticos
    expect(add(5, 0)).toBe(5)                // Si sumas 5 + 0, esperas 5
  })
});

Componente ClickCounter.js

class ClickCounter extends HTMLElement {
  #count = 0

  handleEvent(ev) {
    if (!ev.target.closest("button")) return;
    if (ev.type === "click") this.incr();
  }

  connectedCallback() {
    this.addEventListener("click", this);
    this.render();
  }

  disconnectedCallback() {
    this.removeEventListener("click", this);
  }

  reset() {
    this.#count = 0;
    this.render()
  }
  incr() {
    this.#count++;
    this.render();
    const event = new CustomEvent("count-changed", {
      detail: this.#count
    });
    this.dispatchEvent(event);
  }

  render() {
    this.innerHTML = /* html */`<button>
      Clicks: ${this.#count}
    </button>`;
  }
}

customElements.define("click-counter", ClickCounter);

Tests ClickCounter.test.js

import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import "./ClickCounter.js";

describe("ClickCounter", () => {
  let el;

  beforeEach(() => {
    el = document.createElement("click-counter");
    document.body.append(el);
  });

  afterEach(() => el.remove());

  describe("registro y renderizado inicial", () => {
    it("se registra como custom element", () => expect(customElements.get("click-counter")).toBeDefined());
    it("renderiza un botón al conectarse", () => expect(el.querySelector("button")).not.toBeNull());
    it("empieza en 0", () => expect(el.querySelector("button").textContent).toBe("Clicks: 0"));
  });

  /* ... */
});

Core Web Vitals

  • Core Web Vitals → Métricas de Google para medir performance de una web

  • LCP → El contenido más grande (ej: hero)
  • INPt desde interacción hasta respuesta
  • CLS → Salto de layout acumulado
  • FCPt hasta aparecer el primer contenido visible
  • TTFBt hasta el primer byte

Lighthouse / PageSpeed

Preguntas