Optimización de rendimiento
Optimización del rendimiento basada en evidencia, utilizando señales reales de usuario para priorizar y trazas del navegador para diagnosticar. Se centra en la velocidad de carga, la capacidad de respuesta en tiempo de ejecución y la entrega de recursos.
Cómo funciona
- Si una página se puede ejecutar, lee el flujo de trabajo de medición y establece una línea base de campo y laboratorio antes de editar.
- Prioriza las deficientes Core Web Vitals reales. Utiliza una traza de rendimiento de DevTools y sus informaciónes enfocadas para encontrar la causa.
- Inspecciona y cambia solo el código o los activos relacionados con los cuellos de botella medidos.
- Vuelve a ejecutar mediciones de laboratorio equivalentes e informa los valores antes/después, las condiciones y la incertidumbre. La verificación de campo queda pendiente hasta que lleguen suficientes datos de nuevos usuarios.
Cuando no exista una página ejecutable, realiza una inspección estática, pero llama a los hallazgos hipótesis, no regresiones medidas. Incluye el comando o el flujo de trabajo del navegador que puede verificar cada hipótesis de alto impacto.
Prefiere una herramienta de navegador que registre una traza de rendimiento y exponga informaciónes enfocadas. Con Chrome DevTools MCP, usa performance_start_trace y performance_analyze_insight; no enrutes el rendimiento a través de lighthouse_audit, que cubre categorías de Lighthouse no relacionadas con el rendimiento.
Presupuesto de rendimiento inicial
Los presupuestos deben reflejar los dispositivos objetivo, las redes, los tipos de página y los recorridos del usuario del producto. Los valores a continuación son barreras de protección iniciales para una página típica de contenido o comercio, no criterios universales de aprobado/reprobado. Conserva un presupuesto de proyecto existente cuando ya esté definido.
| Recurso | Presupuesto | Justificación |
|---|---|---|
| Peso total de la página | < 1.5 MB | Limita el tiempo de transferencia y el costo de datos en redes objetivo restringidas; calibra con páginas representativas |
| JavaScript (comprimido) | < 300 KB | Protege el costo de análisis y ejecución |
| CSS (comprimido) | < 100 KB | Limita el trabajo de bloqueo de renderizado |
| Imágenes (sobre el pliegue) | < 500 KB | Protege los recursos probables de LCP |
| Fuentes | < 100 KB | Limita la transferencia de fuentes críticas |
| Terceros | < 200 KB | Limita el código fuera del control del producto |
Ruta de renderizado crítica
Respuesta del servidor
- TTFB < 800ms. El Time to First Byte debe ser rápido. Usa CDN, almacenamiento en caché y backends eficientes.
- Habilita la compresión. Gzip o Brotli para activos de texto. Se prefiere Brotli (15-20% más pequeño).
- HTTP/2 o HTTP/3. La multiplexación reduce la sobrecarga de la conexión.
- Almacenamiento en caché en el borde. Almacena en caché HTML en el borde de la CDN cuando sea posible.
- Considera Early Hints (HTTP 103) para la latencia del documento medida. Si una traza muestra una generación lenta de HTML y subrecursos críticos estables, envía un
103interino con cabecerasLinkantes de la respuesta final normal de la misma solicitud. Usa HTTP/2 o posterior. Una CDN puede sintetizar el103a partir de cabecerasLinken un200anterior, o el controlador de origen/borde puede emitirlo directamente. Los clientes no compatibles continúan con la respuesta final, pero confirma la compatibilidad actual del navegador y la infraestructura. Limita las sugerencias a precargas o preconexiones críticas probadas: las sugerencias inexactas desperdician ancho de banda. Cloudflare informó una mejora del 20-30% en LCP en una prueba artificial con mucho contenido de imagen; trátalo como un caso de estudio del proveedor, no como un ahorro esperado, y mide tu resultado. Consulta el ejemplo de implementación de 103 en MDN y el estudio de Cloudflare.
Carga de recursos
Preconexión a orígenes requeridos:
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://cdn.example.com" crossorigin>
Precarga de recursos críticos:
Precarga solo aquellos recursos cuyo descubrimiento tardío sea visible en la traza. Cada precarga compite por el ancho de banda y una solicitud de alta prioridad innecesaria puede retrasar el LCP.
<!-- Imagen LCP -->
<link rel="preload" href="/hero.webp" as="image" fetchpriority="high">
<!-- Fuente crítica -->
<link rel="preload" href="/font.woff2" as="font" type="font/woff2" crossorigin>
Prerenderiza las navegaciones probables siguientes con la API de Reglas de Especulación:
<script type="speculationrules">
{
"prerender": [{
"where": { "href_matches": "/*" },
"eagerness": "moderate"
}]
}
</script>
moderate espera una señal de intención más fuerte que los modos eager. Mide la tasa de aciertos de predicción, los bytes transferidos y el costo del servidor; un prerrenderizado incorrecto es aproximadamente una navegación no utilizada. Consulta core-web-vitals → LCP para conocer las compensaciones y la compuerta prerenderingchange necesaria para la analítica.
Difiere el CSS no crítico:
<!-- CSS crítico en línea -->
<style>/* Estilos sobre el pliegue */</style>
<!-- CSS no crítico -->
<link rel="preload" href="/styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/styles.css"></noscript>
Optimización de JavaScript
Difiere scripts no esenciales:
<!-- Bloqueo del analizador (evitar) -->
<script src="/critical.js"></script>
<!-- Diferido (preferido) -->
<script defer src="/app.js"></script>
<!-- Asíncrono (para scripts independientes) -->
<script async src="/analytics.js"></script>
<!-- Módulo (diferido por defecto) -->
<script type="module" src="/app.mjs"></script>
Patrones de división de código:
// División basada en rutas
const Dashboard = lazy(() => import('./Dashboard'));
// División basada en componentes
const HeavyChart = lazy(() => import('./HeavyChart'));
// División basada en funciones
if (user.isPremium) {
const PremiumFeatures = await import('./PremiumFeatures');
}
Mejores prácticas de tree shaking:
// ❌ Importa toda la biblioteca
import _ from 'lodash';
_.debounce(fn, 300);
// ✅ Importa solo lo necesario
import debounce from 'lodash/debounce';
debounce(fn, 300);
Optimización de imágenes
Selección de formato
| Formato | Caso de uso | Soporte del navegador |
|---|---|---|
| AVIF | Fotos, mejor compresión | 92%+ |
| WebP | Fotos, buen fallback | 97%+ |
| PNG | Gráficos con transparencia | Universal |
| SVG | Iconos, logotipos, ilustraciones | Universal |
Imágenes responsivas
<picture>
<!-- AVIF para navegadores modernos -->
<source
type="image/avif"
srcset="hero-400.avif 400w,
hero-800.avif 800w,
hero-1200.avif 1200w"
sizes="(max-width: 600px) 100vw, 50vw">
<!-- WebP fallback -->
<source
type="image/webp"
srcset="hero-400.webp 400w,
hero-800.webp 800w,
hero-1200.webp 1200w"
sizes="(max-width: 600px) 100vw, 50vw">
<!-- JPEG fallback -->
<img
src="hero-800.jpg"
srcset="hero-400.jpg 400w,
hero-800.jpg 800w,
hero-1200.jpg 1200w"
sizes="(max-width: 600px) 100vw, 50vw"
width="1200"
height="600"
alt="Imagen principal"
loading="lazy"
decoding="async">
</picture>
Prioridad de imagen LCP
<!-- Imagen LCP sobre el pliegue: carga eager, alta prioridad -->
<img
src="hero.webp"
fetchpriority="high"
loading="eager"
decoding="sync"
alt="Hero">
<!-- Imágenes bajo el pliegue: carga lazy -->
<img
src="product.webp"
loading="lazy"
decoding="async"
alt="Producto">
Optimización de fuentes
Estrategia de carga
/* Pila de fuentes del sistema como fallback */
body {
font-family: 'Custom Font', -apple-system, BlinkMacSystemFont,
'Segoe UI', Roboto, sans-serif;
}
/* Prevenir texto invisible */
@font-face {
font-family: 'Custom Font';
src: url('/fonts/custom.woff2') format('woff2');
font-display: swap; /* o optional para no críticas */
font-weight: 400;
font-style: normal;
unicode-range: U+0000-00FF; /* Subconjunto a Latín */
}
Precarga de fuentes críticas
<link rel="preload" href="/fonts/heading.woff2" as="font" type="font/woff2" crossorigin>
Fuentes variables
/* Un archivo en lugar de múltiples pesos */
@font-face {
font-family: 'Inter';
src: url('/fonts/Inter-Variable.woff2') format('woff2-variations');
font-weight: 100 900;
font-display: swap;
}
Estrategia de almacenamiento en caché
Cabeceras Cache-Control
# HTML (caché corto o sin caché)
Cache-Control: no-cache, must-revalidate
# Activos estáticos con hash (inmutables)
Cache-Control: public, max-age=31536000, immutable
# Activos estáticos sin hash
Cache-Control: public, max-age=86400, stale-while-revalidate=604800
# Respuestas de API
Cache-Control: private, max-age=0, must-revalidate
Almacenamiento en caché de Service Worker
// Cache-first para activos estáticos
self.addEventListener('fetch', (event) => {
if (event.request.destination === 'image' ||
event.request.destination === 'style' ||
event.request.destination === 'script') {
event.respondWith(
caches.match(event.request).then((cached) => {
return cached || fetch(event.request).then((response) => {
const clone = response.clone();
caches.open('static-v1').then((cache) => cache.put(event.request, clone));
return response;
});
})
);
}
});
Rendimiento en tiempo de ejecución
Evitar el layout thrashing
// ❌ Fuerza múltiples reflujos
elements.forEach(el => {
const height = el.offsetHeight; // Lectura
el.style.height = height + 10 + 'px'; // Escritura
});
// ✅ Agrupa lecturas, luego agrupa escrituras
const heights = elements.map(el => el.offsetHeight); // Todas las lecturas
elements.forEach((el, i) => {
el.style.height = heights[i] + 10 + 'px'; // Todas las escrituras
});
Debounce de operaciones costosas
function debounce(fn, delay) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => fn(...args), delay);
};
}
// Debounce de manejadores de scroll/resize
window.addEventListener('scroll', debounce(handleScroll, 100));
Usa requestAnimationFrame
// ❌ Puede causar tirones
setInterval(animate, 16);
// ✅ Sincronizado con la actualización de la pantalla
function animate() {
// Lógica de animación
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
Virtualiza listas largas
// Para listas > 100 elementos, renderiza solo los elementos visibles
// Usa bibliotecas como react-window, vue-virtual-scroller, o CSS nativo:
.virtual-list {
content-visibility: auto;
contain-intrinsic-size: 0 50px; /* Altura estimada del elemento */
}
Navegaciones suaves con View Transitions
La API de View Transitions permite al navegador hacer una transición entre dos estados del DOM con un fundido cruzado (o animación personalizada) usando una única instantánea compuesta por GPU, sin doble renderizado, sin layout thrash, y la instantánea no cuenta para CLS.
Mismo documento (estilo SPA) — Baseline 2026:
// Envuelve la mutación del DOM que intercambia la vista
function navigate(newView) {
if (!document.startViewTransition) return swapDOM(newView);
document.startViewTransition(() => swapDOM(newView));
}
Documento cruzado (estilo MPA) — Chromium estable, mejora progresiva en otros:
/* En ambas páginas, origen y destino */
@view-transition { navigation: auto; }
Esa es toda la integración: las navegaciones del mismo origen ahora se desvanecen automáticamente. Para optar por elementos específicos en transiciones de elementos compartidos (por ejemplo, una miniatura que se expande a una imagen principal), asígnales un view-transition-name coincidente:
.product-thumb[data-id="42"], .product-hero { view-transition-name: product-42; }
Combina esto con Speculation Rules (arriba) para navegaciones instantáneas y animadas.
Scripts de terceros
Estrategias de carga
// ❌ Bloquea el hilo principal
<script src="https://analytics.example.com/script.js"></script>
// ✅ Carga asíncrona
<script async src="https://analytics.example.com/script.js"></script>
// ✅ Retrasar hasta la interacción
<script>
document.addEventListener('DOMContentLoaded', () => {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
const script = document.createElement('script');
script.src = 'https://widget.example.com/embed.js';
document.body.appendChild(script);
observer.disconnect();
}
});
observer.observe(document.querySelector('#widget-container'));
});
</script>
Patrón de fachada
<!-- Muestra un marcador de posición estático hasta la interacción -->
<div class="youtube-facade"
data-video-id="abc123"
onclick="loadYouTube(this)">
<img src="/thumbnails/abc123.jpg" alt="Título del video">
<button aria-label="Reproducir video">▶</button>
</div>
Medición
Usa el flujo de trabajo de medición cada vez que una URL sea ejecutable. Define el enrutamiento de Chrome DevTools MCP, las fuentes CrUX y de respaldo, las condiciones de laboratorio repetibles y un formato de evidencia compacto.
| Métrica | Tipo | Interpretación |
|---|---|---|
| LCP, INP, CLS en p75 | Campo | Core Web Vitals de resultado de usuario; usar para priorización de aprobado/reprobado |
| LCP, CLS en una traza | Laboratorio | Valores de diagnóstico reproducibles para una navegación |
| TBT | Laboratorio | Diagnóstico de bloqueo del hilo principal y un proxy aproximado de INP, no INP de campo |
| FCP, Speed Index | Laboratorio | Diagnósticos de carga, no Core Web Vitals |
Los fragmentos sin procesar de PerformanceObserver son útiles para la sesión actual del navegador, pero no son datos de usuario reales por sí mismos. Cuando el usuario quiera telemetría de producción, lee la referencia de RUM de primera parte y prefiere web-vitals sobre una implementación de métrica hecha a mano.
Referencias
Para optimizaciones específicas de Core Web Vitals, consulta Core Web Vitals.


