Optimización de Performance Web
Mejora Core Web Vitals, reduce tamaños de bundle y optimiza performance frontend con lazy loading, code splitting y herramientas de build modernas.
Visión General
El performance web impacta directamente el engagement de usuarios, tasas de conversión y rankings de búsqueda. Los Core Web Vitals de Google — Largest Contentful Paint (LCP), Interaction to Next Paint (INP) y Cumulative Layout Shift (CLS) — proveen targets medibles. Este recurso cubre técnicas prácticas: lazy loading, code splitting, optimización de imágenes, critical CSS y modern build tooling para lograr cargas de página bajo 3 segundos.
Cuándo Usar
Usa este recurso cuando:
- Los scores de Core Web Vitals están fallando (LCP > 2.5s, CLS > 0.1)
- Usuarios móviles en redes 3G abandonan páginas antes de que carguen
- Los bundle sizes exceden 200KB e impactan el time-to-interactive
- Scripts de terceros (analytics, ads) bloquean el main thread
Solución
Critical CSS Inline + Async Load (HTML)
<head>
<!-- Inline critical CSS (~14KB max) -->
<style>
/* Above-fold styles: header, hero, layout skeleton */
body{margin:0;font-family:system-ui}
.hero{background:#3b82f6;min-height:60vh}
</style>
<!-- Preload key resources -->
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/hero-image.webp" as="image" fetchpriority="high">
<!-- Async load non-critical CSS -->
<link rel="preload" href="/styles.css" as="style" onload="this.rel='stylesheet'">
</head>
Lazy Loading Images con API Nativa
<!-- Native lazy loading — no requiere JavaScript -->
<img src="hero.webp" alt="Hero" fetchpriority="high" width="800" height="400">
<img src="below-fold-1.webp" alt="Product" loading="lazy" width="400" height="300">
<img src="below-fold-2.webp" alt="Team" loading="lazy" width="400" height="300">
Code Splitting con Live Imports (React)
import { lazy, Suspense } from 'react';
const HeavyChart = lazy(() => import('./HeavyChart'));
const AnalyticsDashboard = lazy(() => import('./AnalyticsDashboard'));
function Dashboard() {
return (
<div>
<CriticalStats /> {/* Siempre cargado */}
<Suspense fallback={<Spinner />}>
<HeavyChart /> {/* Cargado on demand */}
</Suspense>
<Suspense fallback={<Spinner />}>
<AnalyticsDashboard /> {/* Chunk separado */}
</Suspense>
</div>
);
}
Explicación
Targets de Core Web Vitals:
| Métrica | Bueno | Malo | Mide |
|---|---|---|---|
| LCP | < 2.5s | > 4s | Tiempo de carga del elemento visible más grande |
| INP | < 200ms | > 500ms | Responsividad de interacciones |
| CLS | < 0.1 | > 0.25 | Estabilidad visual (layout shifts) |
| TTFB | < 600ms | > 1.8s | Time to first byte |
Ejemplo de performance budget:
- JavaScript: 150KB (gzipped)
- Imágenes: 250KB total
- CSS: 50KB (incluyendo critical inline)
- Fonts: 40KB (subsetted)
- Terceros: 100KB máximo
Variantes
| Técnica | Impacto | Esfuerzo |
|---|---|---|
| Optimización de imágenes (WebP/AVIF) | -50% bytes de imagen | Bajo |
| Font subsetting | -80% bytes de font | Bajo |
| Code splitting | -60% JS inicial | Medio |
| Edge caching | -90% TTFB | Bajo |
| Service Worker | Visitas repetidas instantáneas | Medio |
| HTTP/3 + QUIC | Más rápido en redes con pérdida | Bajo (CDN) |
Lo que funciona
- Mide usuarios reales, no tests de lab: Field data de Chrome UX Report refleja condiciones actuales
- Optimiza el critical path: Cualquier cosa bloqueando
<head>debería estar bajo 50KB total. Consulta server-side rendering. - Self-host fonts y analytics: Conexiones de terceros agregan overhead de DNS + TLS + TCP
- Usa
content-visibility: auto: Los browsers skip rendering de contenido off-screen - Defer JavaScript no crítico:
deferotype="module"para scripts no necesarios inmediatamente
Errores Comunes
- Imágenes hero oversized: Un PNG hero de 4MB destruye LCP; usa imágenes responsive con
srcset - Terceros render-blocking: Google Fonts cargados síncronamente retrasan first paint
- Sin resource hints:
preload,prefetchypreconnectson wins de performance gratuitos - Hidratar todo: Arquitectura de islands (Astro, Fresh) envía zero JS para contenido estático
- Ignorar mobile: 70% de usuarios están en mobile; testea en dispositivos reales, no solo DevTools
Lectura Adicional
- Documentación oficial: consulta la referencia actualizada del framework o herramienta utilizada.
- Guías relacionadas: explora las guías de web-performance y performance para profundizar.
- Patrones complementarios: revisa los patrones de diseño aplicables a tu stack tecnológico.
- Postmortems públicos: estudia incidentes reales de equipos que enfrentaron problemas similares en producción.
Notas de Producción
- Despliega gradualmente usando canary o blue-green para detectar regresiones temprano.
- Configura alertas para errores, latencia p99 y tasa de fallos antes de habilitar en producción.
- Documenta el rollback en el runbook; prueba el procedimiento en staging al menos una vez por trimestre.
- Revisa logs estructurados con correlation IDs para trazar requests end-to-end en incidentes.
Puntos Clave
- Aplica optimización de performance web cuando necesites una solución práctica para tu caso de uso.
- Monitorea el rendimiento después de implementar; mide latencia, errores y uso de recursos antes y después.
- Revisa la sección de Troubleshooting ante errores comunes; la mayoría tienen causa raíz documentada con solución.
- Mantén dependencias actualizadas y ejecuta tests en CI para prevenir regresiones en producción.
Ver También
- Guía de Optimización de Performance — estrategias detalladas para backend y frontend
- SPA Code Splitting — lazy loading en single-page apps
- Caching Strategies — reducción de carga backend
- Security Headers — headers que también afectan performance
- Load Testing — testing de performance bajo carga
Última actualización: 2026-07-09
Troubleshooting
- Largest Contentful Paint is high: optimize images, preload critical resources, and reduce server response time.
- JavaScript bundle size grows: analyze the bundle, split code by route, and tree-shake unused dependencies. Lazy-load non-critical components.
- Cache hit rate is low: review cache keys, TTLs, and invalidation patterns.
- Database CPU spikes: find the top queries by execution time and frequency. Add indexes, rewrite queries, or cache results.
- Throughput drops under load: profile for contention, garbage collection, and blocked threads. Scale horizontally only after optimizing the hot path.
Errores Comunes en Producción
- Copiar el ejemplo sin adaptarlo a volúmenes y modos de fallo reales.
- Saltar tests de carga e inyección de errores antes del primer despliegue productivo.
- Codificar valores fijos que deberían ser configurables por entorno.
- Olvidar agregar logging y monitoreo en cada paso.
- Desplegar sin plan de rollback ni estrategia de backup probada.
- Asumir que el ejemplo mínimo escalará sin agregar caché o procesamiento por lotes.
- No documentar la versión y configuración usadas en producción.
- Dejar la receta sin cambios cuando evolucionan las dependencias o la escala.
Preguntas frecuentes
¿Cómo mido Core Web Vitals en producción?
Usa la librería web-vitals de JavaScript para recolectar métricas de usuarios reales: import { onLCP, onINP, onCLS, onTTFB, onFCP } from 'web-vitals';\nonLCP((metric) => sendToAnalytics('LCP', metric.value));\nonINP((metric) => sendToAnalytics('INP', metric.value));\nonCLS((metric) => sendToAnalytics('CLS', metric.value));. Envía data a tu backend de analytics: function sendToAnalytics(name, value) {\n navigator.sendBeacon('/api/vitals', JSON.stringify({ name, value, page: location.pathname }));\n}. Usa el reporte de Core Web Vitals de Google Search Console para field data en tu sitio. Configura alerts para regresiones: si LCP p75 excede 2.5s, triggera un alert. Usa Lighthouse CI en tu pipeline: lighthouse-ci --assertions.lcp=2.5 --assertions.cls=0.1 --assertions.inp=200. Recolecta métricas por page template, no solo promedios site-wide. Segmenta por device type (mobile, desktop, tablet) y connection type (4G, 3G, WiFi). Usa PerformanceObserver para custom metrics: new PerformanceObserver((list) => {\n for (const entry of list.getEntries()) {\n console.log(${entry.name}: ${entry.startTime}ms);\n }\n}).observe({ entryTypes: ['paint', 'largest-contentful-paint'] });.
¿Cómo optimizo tamaños de bundle de JavaScript?
Analiza tu bundle con webpack-bundle-analyzer o rollup-plugin-visualizer: import { visualizer } from 'rollup-plugin-visualizer';\nexport default {\n plugins: [visualizer({ open: true, filename: 'bundle-stats.html' })]\n};. Identifica dependencias grandes y reemplazalas con alternativas más ligeras: moment.js (280KB) → date-fns (20KB), lodash (70KB) → lodash-es con tree shaking. Usa tree shaking: import { debounce } from 'lodash-es'; en lugar de import _ from 'lodash';. Habilita gzip y brotli compression en tu server: gzip on;\ngzip_types text/css application/javascript; en nginx. Code-split por ruta: const About = lazy(() => import('./About')); para reducir el bundle inicial. Usa dynamic imports para features condicionales: if (supportsWebGL) {\n const { render3D } = await import('./3d-renderer');\n render3D();\n}. Audita paquetes de terceros: npm ls --production y remueve dependencias no usadas. Setea límites de bundle size en CI: maxSize: '150KB' para failear builds que exceden budgets. Usa la extensión import-cost de VS Code para ver tamaños de imports durante development. Considera module federation para micro-frontends para compartir dependencias across apps.
¿Cómo optimizo font loading?
Usa font-display: swap para evitar texto invisible: @font-face {\n font-family: 'Inter';\n src: url('/fonts/inter.woff2') format('woff2');\n font-display: swap;\n}. Preload fonts críticos: <link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>. Subset fonts para incluir solo caracteres usados: pyftsubset inter.ttf --text="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" --output-file=inter-subset.woff2. Usa variable fonts para reducir file count: un variable font file reemplaza múltiples weight files. Self-host fonts en lugar de usar Google Fonts CDN para evitar overhead de third-party connection. Usa size-adjust en @font-face para matchear métricas de fallback font: @font-face {\n font-family: 'Inter-fallback';\n src: local('Arial');\n size-adjust: 100%;\n}. Monitorea font loading: document.fonts.ready.then(() => {\n console.log('All fonts loaded');\n});. Usa unicode-range para splitear fonts por script: @font-face {\n unicode-range: U+0000-00FF;\n src: url('/fonts/inter-latin.woff2');\n}.
¿Cómo optimizo imágenes para web performance?
Usa formatos modernos: WebP (30% más pequeño que JPEG) y AVIF (50% más pequeño que JPEG). Sirve imágenes responsive con srcset: <img\n src="hero-800.webp"\n srcset="hero-400.webp 400w, hero-800.webp 800w, hero-1200.webp 1200w"\n sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px"\n alt="Hero"\n fetchpriority="high"\n>. Usa <picture> para format negotiation: <picture>\n <source type="image/avif" srcset="hero.avif">\n <source type="image/webp" srcset="hero.webp">\n <img src="hero.jpg" alt="Hero">\n</picture>. Comprime imágenes: cwebp -q 80 input.jpg -o output.webp para lossy, cwebp -lossless input.png -o output.webp para lossless. Usa blur placeholders para imágenes above-fold: genera una versión tiny blurred (20px wide) y escalala con CSS filter: blur(20px) hasta que la imagen completa carga. Lazy-load imágenes below-fold: <img loading="lazy" src="...">. Setea width y height explícitos para prevenir CLS. Usa CDN image transformation: https://cdn.example.com/image.jpg?w=800&format=webp para servir versiones optimizadas on the fly. Evita usar imágenes para texto — usa CSS en su lugar. Usa SVG para iconos y logos: <img src="logo.svg" alt="Logo">.
¿Cómo reduzco layout shifts (CLS)?
Reserva espacio para imágenes, ads y embeds: <img width="800" height="400" src="..."> previene que el browser reflowee cuando la imagen carga. Usa CSS aspect-ratio: .video-container {\n aspect-ratio: 16 / 9;\n}. Evita inyectir contenido above contenido existente: banner ads deberían reservar espacio antes de cargar. Usa min-height para áreas de contenido dinámico: .comments {\n min-height: 200px;\n}. Preconnect a origins de terceros: <link rel="preconnect" href="https://cdn.example.com"> para evitar late resource discovery. Usa font-display: optional para fonts no críticos para prevenir font-swap layout shifts. Evita display: none toggling en contenido above-fold. Usa transform y opacity para animaciones en lugar de top, left, width, height — estos no triggerean layout. Setea dimensiones explícitas en iframes: <iframe width="560" height="315" src="...">. Usa content-visibility: auto con contain-intrinsic-size: .card {\n content-visibility: auto;\n contain-intrinsic-size: 200px;\n}.
¿Cómo mejoro Interaction to Next Paint (INP)?
INP mide responsividad a interacciones de usuario. Break long tasks: function processItems(items) {\n // Process in chunks of 50ms\n const chunk = items.slice(0, 50);\n // ... process chunk\n if (items.length > 50) {\n setTimeout(() => processItems(items.slice(50)), 0);\n }\n}. Usa requestIdleCallback para work no urgente: requestIdleCallback(() => {\n // Analytics, reporting, etc.\n});. Debounce scroll y resize handlers: const handleResize = debounce(() => {\n // Expensive layout calculation\n}, 150);\nwindow.addEventListener('resize', handleResize);. Usa scheduler.yield() cuando esté disponible: async function processQueue() {\n for (const item of queue) {\n processItem(item);\n await scheduler.yield(); // Yield to main thread\n }\n}. Evita synchronous layout reads: // Bad: forces layout\nfor (let i = 0; i < items.length; i++) {\n items[i].style.left = ${items[i].offsetLeft + 10}px;\n}\n// Good: batch reads and writes\nconst positions = items.map(item => item.offsetLeft);\nitems.forEach((item, i) => {\n item.style.left = ${positions[i] + 10}px;\n});. Usa Web Workers para CPU-intensive tasks: const worker = new Worker('compute.js');\nworker.postMessage(data);\nworker.onmessage = (e) => updateUI(e.data);. Minimiza JavaScript de terceros que bloquea el main thread. Usa requestAnimationFrame para visual updates: function animate() {\n // Update DOM\n requestAnimationFrame(animate);\n}.
¿Cómo uso resource hints efectivamente?
Usa preload para recursos críticos en la página actual: <link rel="preload" href="/fonts/inter.woff2" as="font" crossorigin>. Usa prefetch para recursos necesarios en la próxima página: <link rel="prefetch" href="/next-page.js">. Usa preconnect para establecer conexiones tempranas: <link rel="preconnect" href="https://api.example.com">. Usa dns-prefetch para optimización solo de DNS: <link rel="dns-prefetch" href="//cdn.example.com">. Usa modulepreload para JavaScript modules: <link rel="modulepreload" href="/app.js">. Prioriza con fetchpriority: <img src="hero.webp" fetchpriority="high"> para above-fold, <img src="below.webp" fetchpriority="low"> para below-fold. Evita overusar preload — cada hint consume bandwidth. Testea con Network tab en DevTools para verificar que los hints funcionan. Usa Speculation Rules API para predictive prefetching: <script type="speculationrules">\n{ "prefetch": [{ "source": "list", "urls": ["/about", "/contact"] }] }\n</script>.
¿Cómo implemento un Service Worker para caching?
Registra el Service Worker: if ('serviceWorker' in navigator) {\n navigator.serviceWorker.register('/sw.js');\n}. Cachea static assets con estrategia cache-first: const CACHE = 'static-v1';\nconst ASSETS = ['/index.html', '/styles.css', '/app.js'];\nself.addEventListener('install', (e) => {\n e.waitUntil(caches.open(CACHE).then(cache => cache.addAll(ASSETS)));\n});\nself.addEventListener('fetch', (e) => {\n e.respondWith(\n caches.match(e.request).then(response => response || fetch(e.request))\n );\n});. Usa network-first para contenido dinámico: self.addEventListener('fetch', (e) => {\n e.respondWith(\n fetch(e.request).catch(() => caches.match(e.request))\n );\n});. Usa stale-while-revalidate para API responses: self.addEventListener('fetch', (e) => {\n e.respondWith(\n caches.open('api-cache').then(cache =>\n cache.match(e.request).then(cached => {\n const fetchPromise = fetch(e.request).then(response => {\n cache.put(e.request, response.clone());\n return response;\n });\n return cached || fetchPromise;\n })\n )\n );\n});. Limpia caches viejos: self.addEventListener('activate', (e) => {\n e.waitUntil(\n caches.keys().then(keys =>\n Promise.all(keys.filter(k => k !== CACHE).map(k => caches.delete(k)))\n )\n );\n});. Usa Workbox para manejo más fácil de Service Workers: import { registerRoute } from 'workbox-routing';\nimport { CacheFirst } from 'workbox-strategies';\nregisterRoute(/\.(?:css|js)$/, new CacheFirst());.
¿Cómo optimizo scripts de terceros?
Carga scripts de terceros asíncronamente: <script src="https://analytics.example.com/js" async></script>. Defer scripts no críticos: <script src="https://widget.example.com/js" defer></script>. Usa loading="lazy" para iframes: <iframe src="https://widget.example.com" loading="lazy">. Self-host scripts de terceros cuando sea posible para evitar DNS lookups adicionales. Usa la librería Partytown para correr scripts de terceros en un Web Worker: <script type="text/partytown" src="https://analytics.example.com/js"></script>. Audita el impacto de terceros con Lighthouse: checkea el audit "Reduce third-party usage". Setea un timeout para scripts de terceros: const script = document.createElement('script');\nscript.src = 'https://widget.example.com/js';\nscript.async = true;\nsetTimeout(() => {\n if (!window.widgetLoaded) {\n script.remove(); // Remove if not loaded in 3s\n }\n}, 3000);. Usa resource hints para dominios de terceros: <link rel="preconnect" href="https://analytics.example.com">. Monitorea execution time de scripts de terceros con Performance Observer: new PerformanceObserver((list) => {\n for (const entry of list.getEntries()) {\n if (entry.name.includes('third-party.com')) {\n console.log(Third-party: ${entry.name} took ${entry.duration}ms);\n }\n }\n}).observe({ entryTypes: ['resource'] });.
¿Cómo configuro performance budgets?
Define budgets en webpack.config.js: performance: {\n hints: 'warning',\n maxAssetSize: 150000, // 150KB\n maxEntrypointSize: 200000 // 200KB\n}. Usa Lighthouse CI budgets: // lighthouserc.js\nmodule.exports = {\n ci: {\n assert: {\n assertions: {\n 'resource-summary:script:size': ['<', 150000],\n 'resource-summary:stylesheet:size': ['<', 50000],\n 'resource-summary:image:size': ['<', 250000]\n }\n }\n }\n};. Usa size-limit para library bundles: // package.json\n"size-limit": [\n { "path": "dist/index.js", "limit": "10KB" }\n]. Monitorea budgets en CI: npx size-limit failea el build si se excede. Trackea budgets over time con Bundlephobia o Bundle Analyzer. Setea budgets por ruta, no solo site-wide. Incluye scripts de terceros en budgets. Revisa budgets trimestralmente y ajusta según sea necesario.
Recursos Relacionados
Guía de Optimización de Performance Web
manual detallado para optimizar el rendimiento de aplicaciones web con mejores Core Web Vitals y experiencia de usuario.
RecipeRendimiento SPA: Code Splitting y Lazy Loading
Mejora tiempos de carga de single-page applications dividiendo bundles a nivel de ruta y componente, implementando lazy loading con React.lazy e imports en vivo
DocPlantilla de Planificación de Capacidad
Una plantilla reutilizable para planificar capacidad del sistema, estimar crecimiento y prevenir cuellos de botella de rendimiento antes de que ocurran.
GuideGuía de Entrevistas de System Design: Conceptos Clave
Una guía práctica para entrevistas de system design: escalabilidad, bases de datos, caching, load balancing, microservicios y cómo estructurar tu respuesta.
GuideOptimización de Rendimiento SQL
Una guía práctica para optimizar consultas SQL: estrategias de indexación, reescritura de queries, análisis de EXPLAIN plans y anti-patrones comunes a evitar.
GuideOptimización del Bundle JS: Guía Práctica Frontend
Guía práctica para medir y reducir el tamaño del bundle JavaScript con tree shaking, code splitting, reemplazo de dependencias, compresión y budgets en CI.