WA4.2 Rendimiento: cargar rápido y medir¶
Cómo se han comprobado los ejemplos
Los ejemplos con editor en vivo se ejecutan en tu navegador y se han comprobado en un navegador real. El JavaScript sin DOM (cálculo de contraste, debounce, throttle) se ha ejecutado con Node al construir la web y su salida es la real. Las cifras de rendimiento (por ejemplo, los umbrales de Core Web Vitals) las fija Google y pueden revisarse: consulta web.dev para los valores actuales.
Qué mide el usuario: Core Web Vitals¶
Google resume la experiencia de carga en tres métricas, llamadas Core Web Vitals. Los valores «buenos» actuales (al revisarse con el tiempo, mira web.dev):
| Métrica | Qué mide | Bueno |
|---|---|---|
| LCP (Largest Contentful Paint) | Cuánto tarda en verse lo más grande de la pantalla (una imagen, un titular) | ≤ 2,5 s |
| INP (Interaction to Next Paint) | Cuánto tarda la página en responder a un clic o una pulsación | ≤ 200 ms |
| CLS (Cumulative Layout Shift) | Cuánto se mueve el contenido de forma inesperada mientras carga | ≤ 0,1 |
Se miden con Lighthouse (en las herramientas del navegador, pestaña Lighthouse), PageSpeed Insights (una web de Google) o la pestaña Rendimiento (Performance). Y siempre en un móvil medio y con red lenta, que es donde se ve el problema: un ordenador potente con fibra lo disimula todo.
Cómo carga el navegador un script¶
Un <script> normal detiene la lectura del HTML hasta que se descarga y se ejecuta. Se controla con dos atributos:
| Forma | Descarga | Se ejecuta | Orden | Para qué |
|---|---|---|---|---|
<script src> |
Bloquea el HTML | Al llegar | Garantizado | Casi nunca (solo si el HTML depende de él) |
<script defer src> |
En paralelo | Cuando el HTML ya está leído | Respeta el orden | Lo habitual para el código de la página |
<script async src> |
En paralelo | En cuanto llega, sin esperar | No garantizado | Código independiente (estadísticas, anuncios) |
<script type="module"> |
En paralelo | Como defer |
Respeta el orden | Módulos ES |
La recomendación por defecto: defer en el <head> (se descarga cuanto antes sin bloquear nada) o los módulos.
Otras pistas que se dan al navegador en el <head> para ganar tiempo:
| Pista | Para qué |
|---|---|
<link rel="preconnect" href="https://fonts.gstatic.com"> |
Abrir ya la conexión con un servidor que seguro vas a usar |
<link rel="preload" as="image" href="portada.webp"> |
Pedir ya un recurso importante que el navegador descubriría tarde |
<link rel="stylesheet"> |
Las hojas de estilo bloquean el dibujado (deben estar, pero que sean pequeñas) |
Imágenes: lo que más pesa¶
Las imágenes suelen ser más de la mitad del peso de una página. Con unas pocas decisiones, se reduce mucho:
Qué hace cada pieza:
| Pieza | Efecto |
|---|---|
srcset + sizes |
El navegador elige la versión más pequeña que se vea bien según el ancho de la pantalla y la densidad (retina). 480w son los píxeles reales de ese archivo; sizes dice cuánto ocupará la imagen |
<picture> + <source type> |
Ofrece formatos modernos (AVIF y WebP pesan mucho menos que JPG con la misma calidad) y .jpg como alternativa |
width y height |
El navegador reserva el hueco antes de que la imagen llegue: evita que el contenido «salte» (arregla el CLS) |
loading="lazy" |
No descarga la imagen hasta que se acerca a la pantalla |
decoding="async" |
Decodificarla sin bloquear el resto |
fetchpriority="high" |
Para la imagen principal (la de LCP): descargar antes que el resto |
No pongas loading=lazy a la imagen principal
La imagen grande que se ve nada más abrir la página (la de LCP) debe cargarse cuanto antes. Pon lazy a las que están por debajo del primer pantallazo, no a la de arriba.
Trabajo del JavaScript: no más de lo necesario¶
El hilo principal solo puede hacer una cosa a la vez (lo viste en la WA1): si un script tarda 300 ms, la página no responde durante 300 ms. Eventos como scroll, resize o input se disparan decenas de veces por segundo, y ejecutar un cálculo en cada uno es receta para el desastre. Dos técnicas lo controlan:
| Técnica | Qué hace | Ejemplo típico |
|---|---|---|
debounce |
Espera a que pase un tiempo sin nuevas llamadas y entonces ejecuta una vez | Buscar al terminar de escribir |
throttle |
Ejecuta como mucho una vez cada X ms | Reaccionar al scroll |
// debounce: «espera a que dejen de llamarme»
function debounce(fn, ms) {
let id;
return (...args) => { clearTimeout(id); id = setTimeout(() => fn(...args), ms); };
}
// throttle: «como mucho una vez cada ms». El reloj se inyecta para poder probarlo sin esperar
function throttle(fn, ms, ahora = Date.now) {
let ultima = -Infinity;
return (...args) => {
const t = ahora();
if (t - ultima >= ms) { ultima = t; return fn(...args); }
};
}
// --- debounce: 5 llamadas muy seguidas, una pausa y otra llamada ---
const llamadasD = [];
const buscar = debounce(texto => llamadasD.push(texto), 60);
for (const t of ["k", "ko", "kot", "kotl", "kotli"]) { buscar(t); await new Promise(r => setTimeout(r, 10)); }
await new Promise(r => setTimeout(r, 150));
buscar("kotlin"); await new Promise(r => setTimeout(r, 150));
console.log("debounce ejecutó con:", llamadasD);
// --- throttle con un reloj de mentira ---
let reloj = 0;
const ejecuciones = [];
const alScroll = throttle(pos => ejecuciones.push(pos), 100, () => reloj);
for (const [t, pos] of [[0, "a"], [30, "b"], [60, "c"], [100, "d"], [130, "e"], [250, "f"], [260, "g"]]) {
reloj = t; alScroll(pos);
}
console.log("throttle (100 ms) dejó pasar:", ejecuciones);
Salida (ejecutado con Node):
debounce ejecutó con: [ 'kotli', 'kotlin' ]
throttle (100 ms) dejó pasar: [ 'a', 'd', 'f' ]
En el debounce, las cinco pulsaciones seguidas dan una sola ejecución con la última («kotli»), y la llamada posterior otra. En el throttle, de siete llamadas pasan solo las que están separadas al menos 100 ms: la primera, la de t=100, y la de t=250. (Esta versión de throttle ejecuta la primera de cada ventana y descarta el resto; hay variantes que también ejecutan la última.)
Para el scroll y las animaciones, requestAnimationFrame
Cuando se trata de dibujar (mover algo al hacer scroll), lo ideal no es un tiempo fijo sino sincronizarse con el repintado del navegador: requestAnimationFrame(() => ...) ejecuta tu código justo antes de cada fotograma (normalmente 60 veces por segundo). Y para saber si algo es visible, IntersectionObserver (visto en la WA2) es mejor que escuchar el scroll.
No leer y escribir el diseño a la vez¶
Pedirle al navegador una medida (offsetHeight, getBoundingClientRect()) le obliga a calcular el diseño en ese mismo instante. Si alternas leer y escribir en un bucle, lo recalcula en cada vuelta. La solución es agrupar: primero todas las lecturas, luego todas las escrituras.
// Sin ejecutar: ejemplo ilustrativo del patrón en el navegador
// MAL: lee y escribe alternando; fuerza un recálculo del diseño en cada vuelta
for (const caja of cajas) {
caja.style.height = caja.offsetHeight * 2 + "px";
}
// BIEN: todas las lecturas primero y todas las escrituras después
const alturas = cajas.map(c => c.offsetHeight);
cajas.forEach((c, i) => { c.style.height = alturas[i] * 2 + "px"; });
Caché, compresión y archivos pequeños¶
La carga más rápida es la que no se hace. En el servidor (y en GitHub Pages ya viene resuelto en gran parte):
| Técnica | Efecto |
|---|---|
Caché del navegador (Cache-Control) |
Los archivos que no cambian no se vuelven a descargar |
Nombres con huella (estilos.a1b2c3.css) |
Cambia el nombre cuando cambia el contenido: se puede cachear para siempre sin servir versiones viejas |
| Compresión (gzip, Brotli) | El texto (HTML, CSS, JS) viaja mucho más pequeño |
| Minificar | Quitar espacios y comentarios de CSS y JS |
| Menos peticiones y menos JavaScript de terceros | Cada script externo es un riesgo de rendimiento y de privacidad |
Errores frecuentes¶
| Error | Cómo evitarlo |
|---|---|
<script src> sin defer en el <head> |
defer (o type="module") |
Imágenes sin width y height |
Darlas siempre: evita el salto de diseño (CLS) |
| Subir fotos de 5 MB para mostrarlas a 300 px | srcset, tamaños adecuados y WebP/AVIF |
loading="lazy" en la imagen principal |
fetchpriority="high" para esa |
Cálculo pesado en cada evento scroll o input |
debounce, throttle o IntersectionObserver |
| Alternar lecturas y escrituras del DOM en un bucle | Agrupar: leer todo, luego escribir todo |
| Optimizar «a ojo» | Medir con Lighthouse antes y después |
| Probar solo en tu ordenador con fibra | Simular un móvil y red lenta en las herramientas del navegador |
Para practicar¶
Los ejercicios WA4.3, WA4.4 y WA4.6 de WA4 · Ejercicios practican debounce, throttle y una imagen responsiva.