WooCommerce por defecto se atasca antes de que el catálogo empiece a ser interesante. Una instalación limpia con 200 SKUs vuela en staging, y la misma plantilla con 30k productos y 50k pedidos se mueve en TTFB de 1,5 a 3 segundos, los cart fragments serializan en el archivo de tienda y la fila autoload de wp_options cruza silenciosamente los 100k registros.
La plataforma no es el problema. WooCommerce mueve tiendas españolas con facturaciones de siete cifras anuales. Lo que mata el rendimiento es la configuración por defecto y la acumulación de plugins durante años.
Esta es una guía de campo para endurecer WooCommerce contra carga real, con apuntes específicos del mercado .es: integración con Bizum y Redsys, plugins de SEUR/MRW, hosting tipo Webempresa o Raiola, y el patrón de tráfico de campañas de Black Friday.
Migración a HPOS en WooCommerce y qué cambia
Durante una década WooCommerce guardaba pedidos como posts shop_order en wp_posts, con cada línea, dirección y campo de metadatos explotado en filas dentro de wp_postmeta. Una consulta tan simple como “pedidos de mayo” obligaba a escanear una tabla mezclada con revisiones de blog y borradores, y a hacer JOIN tres o cuatro veces contra postmeta para reconstruir cada pedido.
High Performance Order Storage traslada los pedidos a wc_orders, wc_order_addresses, wc_order_operational_data y wc_orders_meta. Las consultas de listado golpean columnas indexadas directamente en lugar de reconstruir cada fila desde postmeta.
HPOS es el modo por defecto en instalaciones nuevas desde WooCommerce 8.2, publicado en octubre de 2023, según la documentación de HPOS. Las tiendas que arrancaron antes de esa versión siguen a menudo sobre el almacenamiento en posts, y por eso la migración sigue siendo el primer punto de casi cualquier auditoría.
Lo que HPOS no arregla solo:
- Plugins españoles que escriben directamente en claves postmeta como
_factura_serieo_albaran_seurse desincronizarán en silencio cuando desactives el modo compatibilidad - Plugins de facturación tipo WooCommerce PDF Invoices o conectores con Sage que leen pedidos con
get_post_meta($order_id, ...)en vez de$order->get_meta()devolverán vacío - Conectores ERP con Holded o A3 basados en
WP_Querycontrashop_ordernecesitan una reescritura
Flujo de trabajo:
- WooCommerce -> Ajustes -> Avanzado -> Funciones, activa High Performance Order Storage con modo compatibilidad
- Lanza la sincronización fuera de horas punta. En una tienda con 200k pedidos puede tardar varias horas
- Verifica con
wp wc hpos verify_data - Mantén la compatibilidad activa durante una semana mientras pruebas todos los plugins que tocan pedidos
- Desactiva la compatibilidad solo cuando hayas confirmado que cada plugin lee desde HPOS
wp wc hpos status
wp wc hpos sync --batch-size=500
wp wc hpos verify_dataComandos WP-CLI de HPOS desde WooCommerce 8.9
Los tutoriales antiguos usan el espacio de nombres wp wc cot. WooCommerce 8.9.0 lo marcó como obsoleto: los comandos viejos siguen funcionando, pero cada uno imprime un aviso con su sustituto en wp wc hpos, y verify_cot_data pasó a llamarse verify_data (CLIRunner.php). La documentación de las herramientas CLI de HPOS y los docblocks de los comandos recogen el conjunto actual:
wp wc hpos statusmuestra la configuración de HPOS y el estado de la sincronizaciónwp wc hpos count_unmigratedcuenta los pedidos que aún esperan sincronizaciónwp wc hpos syncmigra pedidos por lotes, y--batch-sizevale 500 por defectowp wc hpos verify_datacompara ambos almacenes; su opción--re-migratevuelve a sincronizar los pedidos con diferencias y puede sobrescribir un pedido, así que pruébala antes en stagingwp wc hpos diff 123muestra las diferencias de un solo pedidowp wc hpos compatibility-mode enableydisableactivan o desactivan la sincronización sin abrir el panelwp wc hpos cleanupelimina los datos antiguos en posts de los pedidos migrados. La documentación lo califica de destructivo y, sin argumentos, no hace nada;wp wc hpos cleanup allprocesa todos los pedidos
Lanza cleanup solo cuando el modo compatibilidad lleve desactivado el tiempo suficiente para que ningún plugin lea la copia antigua, y siempre después de una copia de seguridad de la base de datos.
Cómo hacer compatible con HPOS un plugin propio
WooCommerce avisa al dueño de la tienda si intenta activar HPOS con un plugin activo que se ha declarado incompatible, y muestra el mismo aviso en la pantalla de plugins. En plugins que no son extensiones de WooCommerce el aviso solo aparece si la cabecera del plugin incluye la línea WC tested up to. Un plugin propio declara su estado en su archivo principal, como muestra el recetario de HPOS:
add_action( 'before_woocommerce_init', function() {
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true );
} );La declaración es una promesa, no un arreglo. El código que hay detrás tiene que cargar pedidos con wc_get_order() en lugar de get_post() y escribir metadatos a través del objeto pedido (update_meta_data(), add_meta_data(), delete_meta_data() y después save()) en lugar de update_post_meta(). Así WooCommerce decide qué tablas reciben la escritura. El recetario advierte de que save() es una operación relativamente cara, de modo que un bucle que guarda tras cada cambio de metadato es un problema de rendimiento en sí mismo. El código que necesite distinguir el motor de almacenamiento puede llamar a OrderUtil::custom_orders_table_usage_is_enabled().
Cómo desactivar los cart fragments de WooCommerce
Pasa por GTmetrix o WebPageTest una tienda WooCommerce antigua, o una con widget de carrito en la cabecera, y verás ?wc-ajax=get_refreshed_fragments apareciendo en cada página, normalmente comiéndose entre 800 y 1500 ms. Es WooCommerce preguntando “ha cambiado el icono del carrito en la cabecera?” en la home, en el blog, en categorías, en la página de contacto. Aunque el carrito esté vacío.
Caso real: una tienda de moda con checkout sobre Stripe e integración Bizum llegó al Black Friday con un pico de tráfico pagado en torno a 200 sesiones simultáneas. El checkout en sí aguantaba bien, pero los cart fragments serializaban a través de admin-ajax.php justo en el archivo de tienda, donde aterrizaba la mayor parte del tráfico de Meta Ads. Los workers de PHP-FPM en el plan WC Pro de Webempresa hicieron cola, el TTFB del archivo se fue a 8 segundos y la conversión cayó durante una hora y media hasta que el equipo limitó los fragments a páginas comerciales con un dequeue duro.
La regla de alcance más simple:
add_action( 'wp_enqueue_scripts', function() {
if ( is_cart() || is_checkout() || is_product() ) {
return;
}
wp_dequeue_script( 'wc-cart-fragments' );
}, 11 );Plugins como Perfmatters o Asset CleanUp exponen esto como un toggle de UI. Cualquier camino vale; el objetivo es el mismo. Los cart fragments solo deben dispararse donde un icono de carrito necesite actualizarse en vivo.
Si necesitas un contador en cada página, guárdalo en localStorage y actualiza desde JavaScript en los eventos de añadir al carrito en lugar de consultar al servidor.
Cuándo carga WooCommerce los cart fragments desde la versión 7.8
WooCommerce 7.8 cambió el comportamiento por defecto. Según la entrada del blog de desarrolladores de WooCommerce Best practices for the use of the “cart fragments” API (16 de junio de 2023), el script ya no se encola en todas las rutas de la tienda. Sigue cargándose en tres casos: en páginas donde se muestra el widget de carrito, cuando un script de terceros registra el script de cart fragments como dependencia y cuando el código lo encola de forma explícita. La misma entrada señala que algunos temas, entre ellos Storefront, llevan el widget fijo en sus plantillas.
En una tienda actual, la petición en la cascada es por tanto un síntoma con una causa localizable. Busca el handle wc-cart-fragments en el código fuente de la página y rastrea qué plantilla del tema o qué plugin lo carga. Desencolarlo sin esa comprobación puede romper un carrito de cabecera que el tema aún refresca mediante fragments, y un plugin que declara el script como dependencia lo volverá a encolar de todos modos.
Latencia de Bizum y Redsys en el checkout
Bizum integrado vía Redsys añade su propia latencia en el flujo de checkout. La negociación 3DS2 contra el ACS del banco emisor suele estar entre 800 y 1500 ms, fuera del control del servidor. Esto importa porque cualquier cart fragment ejecutándose en paralelo durante el checkout multiplica la percepción de lentitud justo en el momento más crítico. Limita los fragments a la página de carrito y excluye /finalizar-compra/ por completo del polling.
Object cache frente a transients en base de datos
WooCommerce usa transients de forma intensiva para rangos de precios de variaciones, búsquedas de términos de atributos y cálculos de envío. Sin un object cache, los transients viven en wp_options. Que se carguen en cada petición depende de la caducidad: set_transient() guarda un transient sin caducidad como autoload y uno con caducidad sin autoload, más una fila _transient_timeout_ aparte. WooCommerce guarda sus transients de precios de variaciones y de variaciones hijas con una caducidad de 30 días (almacén de datos de productos variables), así que sobre todo inflan el número de filas de wp_options. El peso del autoload viene de las opciones que los plugins escriben sin caducidad.
Dos modos de fallo dignos de mención:
- Una librería online con 12k productos variables tenía transients
wc_var_prices_*acumulándose enwp_optionsdurante dos años. La tabla pasó de los 80 MB. Cada carga sin cache hacía un SELECT sobre unawp_optionscon 130k filas solo para arrancar WordPress. El TTFB se quedaba en 2,4 segundos antes de ejecutar lógica de negocio - Una tienda B2B sobre el plan WordPress Optimizado de Raiola corría con 30+ plugins y un TTFB base de 1,8 s. Tras identificar cuatro plugins (entre ellos un conector con Holded mal configurado) que escribían opciones autoloaded en cada guardado de producto y desactivarlos, el TTFB bajó a unos 600 ms sin tocar nada más
Elige un backend de cache y comprométete. O Redis o Memcached como object cache real, o quédate con transients en base de datos. Mezclar ambos genera bugs de invalidación difíciles de diagnosticar.
# Auditar tamaño autoload en wp_options
wp db query "SELECT SUM(LENGTH(option_value)) AS bytes, COUNT(*) AS rows FROM wp_options WHERE autoload IN ('yes', 'on', 'auto-on', 'auto');"
# Localizar a los peores ofensores
wp db query "SELECT option_name, LENGTH(option_value) AS bytes FROM wp_options WHERE autoload IN ('yes', 'on', 'auto-on', 'auto') ORDER BY bytes DESC LIMIT 20;"Con Redis configurado a nivel de host, ya sea en el panel WC Pro de Webempresa, en OVH Hispano con Redis gestionado o en una VPS de Raiola con Redis instalado a mano, y object-cache.php colocado, set_transient() escribe en el object cache en lugar de en wp_options, los transients se mueven a RAM, baja la presión sobre autoload y el render de una página de producto en cache fría suele bajar del rango 500 a 800 ms a menos de 100 ms.
Valores de autoload en WordPress 6.6 y el aviso de 800 KB
WordPress 6.6 cambió la forma de guardar el autoload. La nota para desarrolladores del núcleo Options API: disabling autoload for large options introduce los valores on, off, auto, auto-on y auto-off; on y off son decisiones explícitas, mientras que auto-on y auto-off los fija el propio WordPress. La función wp_autoload_values_to_autoload() devuelve yes, on, auto-on y auto, y por eso las consultas de auditoría de arriba filtran por esa lista. Una consulta que solo compruebe autoload = 'yes' pasa por alto todas las opciones guardadas tras la actualización y presenta la tienda más limpia de lo que está.
El cambio trajo dos umbrales. Una opción de más de 150 000 bytes no se carga con autoload salvo que el código lo pida de forma explícita, y el filtro wp_max_autoloaded_option_size mueve ese límite. Salud del sitio marca la instalación cuando el tamaño total de las opciones con autoload supera 800 KB. Esa comprobación de Salud del sitio es la primera prueba más barata en una tienda desconocida, porque no necesita acceso por shell y nombra el problema antes de abrir ningún cliente de base de datos.
Excepciones de cache y qué nunca debe cachearse
El cache estático ayuda a archivos, fichas de producto y entradas de blog. Rompe activamente cualquier cosa que dependa de la sesión del usuario.
Exclusiones obligatorias para cualquier cache de página o Page Rule de Cloudflare:
/carrito/o/cart//finalizar-compra/o/checkout//mi-cuenta/o/my-account/- Cualquier URL con
?wc-ajax= - Cualquier URL con la cookie
woocommerce_items_in_cart - Endpoints REST bajo
/wp-json/wc/
Cloudflare APO puede cachear HTML para usuarios no logueados y respeta las cookies de sesión de WooCommerce si se configura bien, pero verifica con curl -I sobre carrito y checkout que recibes cf-cache-status: BYPASS. Una cache mal configurada que sirva el carrito de otra clienta no es una victoria de rendimiento, es una incidencia ante la AEPD.
Plugins de envío que ralentizan el checkout de WooCommerce
El ecosistema de envío en España añade peso al checkout de formas que las guías genéricas pasan por alto.
- Plugins oficiales de SEUR, MRW y GLS suelen consultar la API del transportista en cada cambio de dirección durante el checkout. Activa el cache local de tarifas siempre que el plugin lo permita
- Plugins de impresión de etiquetas que se enganchan a la transición de estado del pedido pueden disparar peticiones síncronas desde
woocommerce_order_status_changed. Mueve esos hooks a Action Scheduler para que no bloqueen el cierre del pedido - Conectores con marketplaces tipo PrestaShop importado o Tiendanube en migración suelen dejar metadatos huérfanos en postmeta tras desinstalar; revísalo con
wp db queryantes de mover a HPOS
Consultas de productos variables y woocommerce_variable_children_args
Productos variables con 50+ variaciones cada uno son un multiplicador de JOIN. El archivo de una tienda de moda con 200 productos variables y 30 variaciones cada uno está haciendo JOIN contra wp_posts por 6000 productos hijos en cada carga.
Dos arreglos prácticos:
- Limita las variaciones cargadas en el desplegable a las que están en stock y son visibles
- Usa
woocommerce_variable_children_argspara excluir variaciones privadas y sin stock de la carga inicial en archivos
add_filter( 'woocommerce_variable_children_args', function( $args ) {
if ( is_shop() || is_product_category() ) {
$args['post_status'] = 'publish';
$args['meta_query'][] = array(
'key' => '_stock_status',
'value' => 'instock',
);
}
return $args;
} );Inflado de tablas en Action Scheduler
Cada extensión de WooCommerce que hace trabajo en segundo plano usa Action Scheduler. Suscripciones, emails de seguimiento, recuperación de carrito abandonado, sincronización con ERP, integraciones con Mailrelay para newsletters transaccionales, todo acaba en wp_actionscheduler_actions y wp_actionscheduler_logs.
En un comercio con suscripciones activas y recuperación de carrito sobre Mailrelay o Mailchimp, estas tablas acumulan millones de acciones completadas y fallidas en menos de un año. Y se consultan en cada carga del admin para pintar el contador de la cola.
Script de mantenimiento:
# Borrar acciones completadas y canceladas con más de 31 días
wp action-scheduler clean --before="31 days ago" --pause=1
# Inspeccionar el tamaño de la cola
wp db query "SELECT status, COUNT(*) FROM wp_actionscheduler_actions GROUP BY status;"
# Confirmar que existen índices en status y scheduled_date_gmt
wp db query "SHOW INDEX FROM wp_actionscheduler_actions;"Cuánto tiempo guarda Action Scheduler las acciones completadas y fallidas
Action Scheduler limpia lo suyo durante la ejecución de la cola. Su limpiador de cola borra las acciones completadas y canceladas cuando superan los 31 días, y el filtro action_scheduler_retention_period cambia ese plazo. Desde Action Scheduler 4.0 también se borran las acciones fallidas, a los tres meses por defecto (action_scheduler_retention_period_for_failed); el comentario del código vincula ese plazo al ciclo contable trimestral.
La limpieza corre dentro del ejecutor de la cola. En una tienda que desactiva WP-Cron y no tiene un cron de sistema que llame a wp-cron.php o a wp action-scheduler run, la cola se detiene y la limpieza se detiene con ella, sin ningún error visible. La acumulación que deja ese hueco se puede borrar a mano con wp action-scheduler clean, que según la referencia WP-CLI de Action Scheduler admite --status (por defecto canceled,complete), --before (por defecto 31 días), --batch-size (por defecto 20) y --pause para esperar entre lotes en una base de datos con carga.
Cómo optimizar las imágenes de producto en WooCommerce
Las imágenes de producto suelen ser el payload más pesado. Tres reglas:
- AVIF para hero y galería, con WebP de fallback para versiones antiguas de Safari que sigan apareciendo en tu mix de tráfico
- Deja de permitir que clientes suban exportaciones raw de 4000 px. Limita el pipeline de subida a 2000 px en el lado largo
loading="lazy"nativo basta; elimina cualquier lazy loader JavaScript antiguo que entre en conflicto
Redimensiona antes de subir en lugar de confiar en que WordPress genere miniaturas al vuelo, lo que escribe en wp_postmeta e infla la base de datos.
Limpieza de la base de datos de WooCommerce con WP-CLI
Lanza esto mensualmente, con más frecuencia en tiendas con alto volumen de carrito abandonado:
# Eliminar sesiones expiradas (solo hace falta si la limpieza programada se ha detenido)
wp db query "DELETE FROM wp_woocommerce_sessions WHERE session_expiry < UNIX_TIMESTAMP();"
# Limpiar transients de WooCommerce
wp wc tool run clear_transients --user=1
# Optimizar tablas
wp db optimize
# Comprobar profundidad de la cola Action Scheduler
wp action-scheduler statusPrograma por cron de sistema en lugar de wp-cron.php, que solo se dispara con tráfico. La opción --user de wp wc tool run sigue los ejemplos de la CLI de WooCommerce; usa el ID de una cuenta de administrador de tu instalación.
Cómo limpia WooCommerce las sesiones de clientes
WooCommerce ya poda su tabla de sesiones. Registra una acción recurrente de Action Scheduler, woocommerce_cleanup_sessions, que se ejecuta cada 12 horas (class-woocommerce.php), y el gestor de sesiones borra las filas caducadas en lotes de 100 con una breve pausa entre lotes. La sesión de un invitado caduca a los dos días y la de un cliente con sesión iniciada a los siete; los filtros wc_session_expiration y wc_session_expiring cambian ambos plazos. Las duraciones de más de 30 días dejan un aviso en el registro de WooCommerce, porque las sesiones largas hacen crecer la tabla.
De ahí salen dos consecuencias. Una tabla de sesiones medida en gigabytes suele indicar que la cola de Action Scheduler no corre o que un plugin ha alargado la duración de las sesiones, así que revisa wp action-scheduler status y esos filtros antes de borrar nada. Y nunca lances TRUNCATE sobre wp_woocommerce_sessions en una tienda en producción: vacía todos los carritos activos, mientras que el DELETE ... WHERE session_expiry < UNIX_TIMESTAMP() de arriba solo elimina filas ya caducadas.
Qué métricas de rendimiento medir en WooCommerce
Optimizar a ciegas lleva a optimizar lo que no toca. Elige tres señales y vigílalas semanalmente:
- TTFB en cache fría sobre el archivo de tienda y una ficha de producto
- Tiempo hasta el primer byte en
/carrito/y/finalizar-compra/con uno y cuatro artículos en el carrito - Recuento de filas y bytes totales en wp_options autoload
- Profundidad de cola Action Scheduler y acción pendiente más antigua
Query Monitor en staging te dice qué consultas dominan. New Relic o Datadog APM en producción muestran la distribución real. PageSpeed Insights es una prueba de humo, no un diagnóstico.
Última actualización
2026-10-14. Notas de campo de auditorías reales sobre WooCommerce en el último año, incluyendo tiendas alojadas en Webempresa, Raiola y OVH Hispano. Si tu tienda no encaja con estos patrones, los patrones no son el problema. Empieza por leer el slow query log.
Necesitas un desarrollador WooCommerce o optimización de velocidad WordPress? Hablemos.







