Caso de éxito · Retail B2B

2.942 consultas por ficha de producto. Ahora 199.

Cómo un retailer B2B multitienda bajó el Speed Index de su ficha de producto de 7 segundos a menos de uno: primero la base de datos, después la caché y, al final, el frontend.

Cliente
Retailer B2B, más de 20 M€ al año
Plataforma
Magento 2 · 8 tiendas
Catálogo
Más de 20.000 SKU
Foco
Ficha de producto
Alcance
Backend, Varnish, frontend, Hyvä
Consultas SQL · ficha
2,942199
−93%
Tiempo SQL · ficha
719ms98ms
−87%
LCP · usuarios reales, escritorio
4.1s1.7s
−59% · Bueno
Peso de página · escritorio
4.4MB496KB
−89% · Hyvä, preproducción
El problema

Una ficha de producto, construida como veinte.

Un retailer B2B que vende más de 20 M€ al año tiene ocho tiendas en una sola instalación de Magento. Sus fichas de producto son muy completas: stock por almacén, accesorios, packs, documentos, vídeos. También se habían vuelto lentas. Sin caché, una ficha tardaba casi 10 segundos de servidor, lanzaba 2.942 consultas SQL y cargaba completos unos 20 productos para mostrar uno.

Los datos de usuarios reales lo confirmaban. En escritorio, el LCP del percentil 75 era de 4,1 s y el tiempo hasta el primer byte, de 3,5 s. Google Search Console no dejaba de marcar URLs lentas. En los picos de tráfico, una consulta pesada provocaba deadlocks en la base de datos.

Core Web Vitals, usuarios reales, antesCrUX, escritorio, percentil 75
4.1s
LCP (bueno ≤ 2,5 s)
3.5s
TTFB (bueno ≤ 0,8 s)
0.12
CLS (bueno ≤ 0,1)
Suspendido
Evaluación Core Web Vitals
Capítulo 1 · Base de datos y PHP

Primero el servidor. La caché, después.

La caché esconde una página lenta, pero cada fallo de caché, cada purga y cada URL nueva lo siguen pagando. Perfilamos fichas sin caché con Blackfire y revisamos las estadísticas de consultas de la propia base de datos. Después arreglamos cada bloque de la página, uno a uno.

  • Stock: una consulta en lugar de cientos.El stock se leía con un SELECT por cada origen de inventario y por cada producto. Con 20 orígenes y 30 productos en una página, son 600 consultas. Ahora una sola consulta con JOIN devuelve el stock de todos los productos y distingue el almacén propio de los proveedores en dropshipping.
  • Una consulta pesada de una extensión, en caché.Una extensión de adjuntos de producto lanzaba una consulta tan lenta que provocaba deadlocks en los picos de tráfico. Reescribimos la consulta y guardamos el resultado en Redis.
  • Tablas planas para las relaciones de producto.Cuatro bloques de productos relacionados y uno de packs cargaban modelos de producto completos. Ahora dos tablas de índice planas (productos simples y packs) guardan lo que esos bloques muestran: precio, stock, URL e imagen. Una tabla de ranking de ventas permite ordenar y limitar en SQL. Reindexar lleva menos de un minuto para los simples y dos para los packs, con más de 20.000 productos y 8 tiendas.
  • Sin recargar datos que la página ya tiene.Los datos estructurados (rich snippets) volvían a cargar productos completos para mostrar valores que ya estaban en la página. Ahora leen los objetos ya construidos, o Redis.
-- before: one query per source, per product, per block (20 × 30 = 600)
SELECT quantity, status FROM inventory_source_item
WHERE sku = ? AND source_code = ?;

-- after: one query per page (simplified)
SELECT si.sku, src.is_dropship, SUM(si.quantity) AS qty
FROM inventory_source_item si
JOIN inventory_source_stock_link l ON l.source_code = si.source_code AND l.stock_id = ?
JOIN inventory_source src ON src.source_code = si.source_code
WHERE si.sku IN (…) AND si.status = 1
GROUP BY si.sku, src.is_dropship;
Ficha sin caché, después como % de antesPerfiles de Blackfire
AntesDespués
Tiempo total
100%
43%
Espera de E/S
100%
41%
CPU
100%
46%
Pico de memoria
100%
86%
Red
100%
52%
Tiempo SQL
100%
14%
Consultas SQL
100%
7%
Ficha de producto sin cachéPerfiles de Blackfire
MétricaAntesDespuésCambio
Tiempo total9.76s4.23s−56%
Espera de E/S5.66s2.33s−58%
CPU4.1s1.9s−53%
Pico de memoria292MB250MB−14%
Red15.0MB7.74MB−48%
Tiempo SQL719ms98ms−87%
Consultas SQL2,942199−93%
Capítulo 2 · Varnish y precalentamiento de caché

Cada página en caché antes de que el primer visitante la pida.

varnishstat mostró que en los picos de tráfico Varnish se quedaba sin memoria y expulsaba páginas cacheadas para hacer sitio a otras nuevas. Le dimos memoria suficiente para todo el catálogo. Después nos aseguramos de que la caché estuviera siempre llena.

Un pequeño módulo de Magento exporta todas las URLs de CMS, categorías y productos de cada tienda. Un precalentador de caché escrito en Rust las pide todas con un límite fijo de peticiones simultáneas, para no sobrecargar nunca los servidores. Magento separa la caché según la cookie X-Magento-Vary, así que el precalentador recorre cada URL una vez por grupo de clientes. La caché se purga cada noche y se vuelve a calentar justo después. Durante el día, el precalentador sigue funcionando.

// warmer core: bounded concurrency over the exported URL list
stream::iter(urls)
    .map(|url| client.get(url).header(COOKIE, &vary_cookie).send())
    .buffer_unordered(MAX_IN_FLIGHT)  // never more than N requests at once
    .for_each(|res| async { record(res) })
    .await;
Precalentador de cachéPor tienda
~15 min
Recorrido completo, caché vacía
1–2 min
Mismo recorrido, caché caliente
2
Grupos de clientes recorridos
8
Tiendas
Capítulo 3 · Frontend y Magepack

De 175 peticiones a 58.

La página hacía 175 peticiones y pesaba 4,4 MB. Varios bloques se cargaban por AJAX después de la propia página, así que esta seguía cambiando durante segundos. El Speed Index era de 7 s y el CLS de 0,65. Demasiadas fuentes, demasiado RequireJS y un script de A/B testing que desvanecía la página cuando ya había cargado.

  • Los bloques que se cargaban por AJAX después de la página ahora vienen en el HTML cacheado.Esto resolvió la mayor parte del Speed Index y de los saltos de layout.
  • JavaScript empaquetado con Magepack.Antes probamos el bundling de Magento y otra extensión. Magepack dio el mejor resultado.
  • La imagen principal carga primero.La primera imagen de la galería carga sin lazy loading, con fetchpriority="high".
  • Los formularios del CRM se cargan bajo demanda.El script de formularios solo se carga cuando el visitante abre un formulario.
  • Los vídeos muestran una miniatura hasta que se hace clic.Los iframes de vídeo solo se cargan tras un clic, en descripciones y en la galería.

Las etiquetas de marketing (A/B testing, gestor de etiquetas, consentimiento) son de otro equipo y se están corrigiendo por separado. Medimos con y sin ellas para que cada equipo viera su parte.

Capítulo 4 · Hyvä

RequireJS era el techo. Hyvä lo quitó.

Magepack llevó el escritorio hasta donde RequireJS y Knockout permitían. En móvil seguíamos alrededor de 50. La ficha de producto ya está rehecha en Hyvä con Alpine.js y está en pruebas.

  • El menú completo se guarda en Redis. Su nivel más profundo se carga al interactuar.
  • Todo el JavaScript, galería incluida, espera a la primera interacción. La imagen LCP se precarga en el <head>.
  • Los modales se renderizan al interactuar, no al cargar.
  • Imágenes recodificadas, iconos SVG aligerados y los iconos repetidos servidos desde un único sprite SVG.
  • La fuente web se limita al subconjunto latino: de 80 KB a 30 KB.
Ficha de producto, escritorio, tres etapasWebPageTest · 5/1 Mbps, 28 ms
TTFB
183ms−24%
Inicio de render
0.32s−81%
FCP
0.52s−73%
LCP
0.67s−82%
Speed Index
0.68s−90%
CLS
0.00−100%
TBT
48ms−82%
Carga completa
1.02s−93%
Peticiones
35−80%
Peso de página
496KB−89%
Bloqueantes
1−97%
Puntuación de rendimiento en PageSpeed Insights, ficha de productoDatos de laboratorio · Lighthouse
67Escritorio · antesMejorable
82Escritorio · MagepackMejorable
95Sin etiquetas de marketingBueno
100Escritorio · HyväBueno
53Móvil · MagepackMejorable
86Móvil · HyväMejorable

Las cifras de antes y de Magepack son mediciones en producción. Las de Hyvä vienen del entorno de preproducción sin etiquetas de terceros y se sustituirán por mediciones en producción tras el lanzamiento.

Usuarios reales, hoy

Los datos de laboratorio son una promesa. Los de usuarios reales, la prueba.

Google posiciona según lo que viven los visitantes reales, medido por Chrome durante 28 días. En escritorio, LCP, FCP e INP ya aprueban, y el tiempo hasta el primer byte es tres veces más rápido que antes.

Core Web Vitals, usuarios reales, antes → ahoraCrUX, escritorio, percentil 75
LCP
4.1s1.7s
−59% · Bueno
FCP
3.8s1.5s
−61% · Bueno
INP
119ms100ms
−16% · Bueno
TTFB
3.5s1.1s
−69%
CLS
0.120.11
A mejorar · ver abajo

El CLS es la única métrica que aún supera el umbral. Un script de marketing añadido después de la optimización desplaza el layout, y se está corrigiendo en el lanzamiento de Hyvä. Por eso el rendimiento necesita monitorización: un solo script nuevo puede deshacer meses de trabajo.

Cómo se hizo

Cuatro fases, cada una medida.

  1. Fase 1Profiling con Blackfire y estadísticas de consultas de RDS. Stock, adjuntos, tablas planas, datos estructurados.
  2. Fase 2Memoria de Varnish dimensionada para los picos. Exportación de URLs y precalentador de caché en producción.
  3. Fase 3Bloques AJAX eliminados, Magepack, imagen LCP, scripts de terceros bajo demanda.
  4. Fase 4Tema Hyvä, empezando por la ficha de producto. Ahora en pruebas.
Qué cambió para el negocio
  • Avisos de URLs lentas en Google Search Console reducidos en más de un 80%.
  • LCP de usuarios reales en escritorio de 4,1 s a 1,7 s.
  • Se acabaron los deadlocks de base de datos en los picos de tráfico.
  • Páginas servidas desde Varnish, en caliente, a todas horas.
  • Puntuación PageSpeed en escritorio de 67 a 95 sin etiquetas de terceros, y 100 con Hyvä.

¿Quieres cifras así? Empieza con una auditoría.