Web Vitals en producción: lo que las métricas no te dicen
Silvano Puccini
Full Stack Engineer
El LCP de FerrelonStock llegó a 4.2 segundos en móvil. Lo supe porque un cliente lo mencionó de pasada en una reunión, no porque tuviéramos un dashboard mirándolo. Ese fue el primer problema.
Performance
El contexto
FerrelonStock es un sistema de gestión de inventario construido con Django en el backend y React en el frontend. El módulo más crítico es el listado de productos — una tabla paginada con filtros, precios en tiempo real y estados de stock. Es la pantalla que los operadores ven cincuenta veces por día.
Cuando empezamos a medir en serio, los números eran malos:
- LCP: 4.2s en conexión 4G simulada
- FID: 380ms en interacción inicial
- CLS: 0.18 — la tabla se desplazaba al cargar
El score de Lighthouse era 34. En producción real, probablemente peor.
El error que cometimos primero
La respuesta instintiva fue: son las imágenes. Revisamos el bundle, encontramos que cargábamos thumbnails sin lazy loading, los agregamos, volvimos a medir.
LCP: 3.9s. Mejora marginal. El problema estaba en otro lado.
Dónde estaba el cuello de botella real
Usamos el panel de Network del navegador con throttling 4G y encontramos algo que no esperábamos: el tiempo de bloqueo del hilo principal era de 1.8 segundos antes de que el primer byte de HTML llegara al navegador.
El problema era el bundle de JavaScript.
React estaba cargando el módulo completo de inventario — editor de productos, módulo de reportes, formularios de alta — todo en la pantalla de listado. Nadie lo había cuestionado porque en desarrollo con fibra óptica no se nota.
"El cuello de botella siempre está donde menos esperás. Si vas directo a las imágenes sin medir el hilo principal, podés pasar semanas optimizando lo que no importa."
Lo que realmente funcionó
1. Code splitting por rutas
Separamos cada módulo en chunks independientes. El listado de productos ya no arrastraba el código del editor ni de los reportes.
// Antes — todo cargaba junto
import ProductEditor from './ProductEditor';
import ReportsModule from './ReportsModule';
// Después — cada módulo carga cuando se necesita
const ProductEditor = lazy(() => import('./ProductEditor'));
const ReportsModule = lazy(() => import('./ReportsModule'));El bundle del listado pasó de 847KB a 312KB. Solo con eso, el LCP bajó a 2.6s.
2. Caching en el servidor
Las llamadas a la API de productos no tenían caché. Cada carga hacía una query fresca a la base de datos con joins a tres tablas. Agregamos caché de 60 segundos con invalidación por evento.
from django.views.decorators.cache import cache_page
from django.utils.cache import get_cache_key
@cache_page(60)
def product_list(request):
queryset = (
Product.objects
.select_related('supplier', 'category')
.prefetch_related('stock_entries')
.filter(active=True)
)
return JsonResponse(serialize(queryset))Con esto, las requests subsiguientes pasaron de 420ms a 45ms.
3. Eliminar una dependencia fantasma
Al auditar el bundle encontramos moment.js — 67KB minificado — incluido por una librería de filtros que usábamos en exactamente un lugar. Lo reemplazamos con dos líneas nativas.
// Antes — 67KB por esto
import moment from 'moment';
const formatted = moment(date).format('DD/MM/YYYY');
// Después — cero dependencias
const formatted = new Intl.DateTimeFormat('es-AR', {
day: '2-digit', month: '2-digit', year: 'numeric'
}).format(new Date(date));Los números finales
| Métrica | Antes | Después |
|---|---|---|
| LCP | 4.2s | 1.8s |
| FID | 380ms | 90ms |
| CLS | 0.18 | 0.03 |
| Lighthouse | 34 | 81 |
Lo que aprendí
Las métricas de Lighthouse en desarrollo local son inútiles. El problema real de FerrelonStock no se veía en ningún entorno local — solo aparecía con throttling real y caché frío en producción.
Antes de optimizar, medí. Antes de medir, instrumenté. Y lo que encontré no fue lo que esperaba encontrar.
Eso es lo interesante de performance: el cuello de botella siempre está donde menos esperás.
Newsletter
¡No te pierdas! Mantenete cerca del radar.
Recibí semanalmente lo que estoy construyendo — artículos, recursos técnicos y reflexiones sobre el futuro del diseño digital. Sin spam, solo arquitectura.