Desde hace unos años hago tanto trabajo para aligerar webs y optimizarlas a todos los niveles, huyendo de soluciones prefabricadas y de software recomendado a base de enlace de referidos, que leo y estudio mucho sobre el tema. Desde la carrera los temas de rendimiento y optimización siempre me gustaron (todavía recuerdo en 5º hacer prácticas con CUDA cuando nadie usaba CUDA y mira a dónde ha llegado). Así que nada, me permito traduciros un post que he leído hace poco, porque hacía tiempo que no me encontraba algo de tanta calidad. El original es este: 30 WordPress & WooCommerce Performance Tips At the Config Level (2026) de Marcin Dudek.
Problemas integrados en los ajustes predeterminados de WooCommerce. No hace falta ningún plugin, solo saber que existen.
En cada página de tu tienda WooCommerce se lanza una petición AJAX oculta llamada wc-ajax=get_refreshed_fragments. Su función es actualizar el icono del carrito en tu cabecera. Y para hacerlo, arranca toda la pila de WordPress + WooCommerce.
Eso supone una ejecución PHP completa en cada carga de página que nadie ve en los tests de rendimiento. Tus usuarios sí la notan, sin embargo. Si no usas un widget de mini-carrito, una sola línea lo soluciona:
wp_dequeue_script('wc-cart-fragments'); Si realmente usas un mini-carrito, al menos limita esto a las páginas de la tienda — no dejes que se dispare en tus entradas del blog. Este único cambio puede llegar a reducir la carga del servidor un 30% en tiendas con un tráfico considerable.
Si usas Redis para la caché de objetos, revisa tu configuración maxmemory-policy. El valor predeterminado suele ser allkeys-lru, lo que significa que cuando Redis se queda sin memoria expulsa cualquier clave — incluidas las sesiones activas del carrito de WooCommerce.
El cliente llena un carrito, navega 10 minutos, va al checkout: carrito vacío. Porque Redis eliminó silenciosamente su sesión durante un pico de tráfico.
maxmemory-policy volatile-lru
Así Redis solo expulsa las claves que tienen TTL asignado. Tus claves de caché persistente sobreviven; las sesiones con fecha de expiración se ciclan de forma natural. Ejecuta también:
redis-cli INFO stats | grep evicted_keys
Si ese número no para de crecer, necesitas más memoria o cachear menos cosas.
El almacenamiento de pedidos de alto rendimiento (HPOS) de WooCommerce mueve los pedidos de las lentas tablas wp_posts/wp_postmeta a tablas dedicadas. Una mejora real. Pero por defecto funciona en modo compatibilidad con la sincronización activada — lo que significa que cada escritura de pedido va a las tablas antiguas y a las nuevas a la vez. No estás reduciendo la carga de la base de datos, la estás duplicando.
Tras verificar que HPOS funciona correctamente y que tus plugins son compatibles, desactiva la sincronización: WooCommerce → Ajustes → Avanzado → Funcionalidades. De un día para otro reducirás a la mitad las escrituras en base de datos relacionadas con pedidos.
WooCommerce mantiene una tabla desnormalizada llamada wp_wc_product_meta_lookup — almacena en caché precios, estado de stock y valoraciones de productos. El problema: si importas productos mediante CSV, usas un editor masivo o sincronizas desde un ERP, la tabla de búsqueda no se actualiza. WooCommerce solo la refresca a través de sus propios hooks.
Así que tus precios son correctos en wp_postmeta, pero la tabla de búsqueda muestra datos antiguos. El orden de los productos es incorrecto, los filtros muestran stock desactualizado y los productos en oferta no aparecen en las consultas de rebajas.
Solución: WooCommerce → Estado → Herramientas → Regenerar tablas de búsqueda de productos. Esto es muy típico en tiendas que hacen cualquier tipo de sincronización de datos externa.
WooCommerce actualiza periódicamente woocommerce_tracker_last_send, _transient_wc_count_comments y varias opciones de seguimiento de uso. Cada una de ellas es una escritura en wp_options sobre una fila con autocarga — lo que invalida toda la caché de alloptions (ver el consejo sobre alloptions).
Aunque hayas desactivado el seguimiento de uso de WooCommerce, algunas de estas escrituras siguen produciéndose. Son escrituras pequeñas, pero en una tienda con mucho tráfico se acumulan provocando una invalidación de caché constante. SAVEQUERIES las encontrará todas.
En mi experiencia, la mayoría de los problemas de rendimiento en WordPress empiezan en la base de datos. Configuraciones por defecto incorrectas, tablas sobrecargadas, índices ausentes. Encuentro esto en prácticamente todas las tiendas que audito.
Ejecuta ahora mismo esta consulta:
SELECT SUM(LENGTH(option_value)) FROM wp_options WHERE autoload='yes';
Si el resultado supera 1 MB, cada carga de página está arrastrando todos esos datos a memoria. WordPress carga todas las opciones con autoload en cada petición, sin excepción.
Los culpables habituales: configuraciones de plugins abandonados que nunca se limpiaron, transients de WooCommerce marcados como autoload=yes por error, opciones antiguas de temas de hace tres rediseños. Y otro que añado, código CSS en donde no debe, en el «Custom CSS» del personalizador o del tema que también se añade con autoload. He visto tiendas con 8 MB de datos en autoload. El propietario del sitio estaba echando la culpa al hosting.
Si todavía estás usando la query_cache de MySQL, desactívala. MySQL la dejó obsoleta en la versión 5.7 y la eliminó en la 8.0 por una buena razón. Cada vez que algo escribe en la base de datos —y WooCommerce escribe en cada pedido, en cada actualización del carrito, en cada cambio de stock— la query cache aplica un bloqueo global (mutex). Todas las consultas tienen que esperar en cola mientras se invalida la caché.
Un solo pedido = cientos de consultas en caché invalidadas al mismo tiempo. La “caché” pasa a ser tu cuello de botella.
query_cache_type = 0
query_cache_size = 0
Y si estás en MariaDB pensando que estás a salvo: es el mismo problema, el mismo bloqueo global (mutex).
El valor por defecto de innodb_buffer_pool_size en MySQL es de 128 MB. Eso apenas alcanza para una instalación limpia de WordPress sin contenido. Si tu base de datos supera los 500 MB (y con WooCommerce probablemente sea así —solo wp_postmeta ya crece muchísimo), MySQL estará leyendo desde disco en prácticamente cada consulta. Esa es la diferencia entre 2 ms y 200 ms por consulta.
La solución: configura innodb_buffer_pool_size al 70–80% de la RAM disponible si tienes un servidor dedicado para la base de datos. Si la aplicación y la base de datos comparten máquina, apunta a alrededor del 40%.
Comprueba el ratio de aciertos (hit ratio) con:
SHOW STATUS LIKE 'Innodb_buffer_pool_read%';
Si Innodb_buffer_pool_reads es más del 1% de Innodb_buffer_pool_read_requests, estás accediendo a disco con demasiada frecuencia.
Probablemente este sea el mayor salto de rendimiento que puedes conseguir, y la mayoría de los hostings lo dejan con el valor por defecto.
Y no, no me refiero a las que aquí vendemos para RedSys o Ceca, sino a otras que conoceréis como Stripe. Comprueba cuántas filas ha dejado Stripe en tu tabla wp_options:
SELECT COUNT(*) FROM wp_options WHERE option_name LIKE '_transient_wc_stripe%';
Stripe, PayPal, Square… todos almacenan en caché las respuestas de sus APIs como transients en filas individuales. Miles de ellas. Están con autoload=no, así que no se cargan en cada página, pero aun así hinchan el tamaño físico de la tabla en disco.
Una tabla más grande implica consultas más lentas para todo lo demás, incluidas las opciones con autoload que sí se cargan en cada petición.
Límpialas, y luego averigua por qué tu tarea programada (cron) de limpieza no está haciendo su trabajo.
Ejecuta esto:
SELECT COUNT(*) FROM wp_woocommerce_sessions;
WooCommerce guarda el carrito completo y los datos del cliente como blobs serializados en PHP para cada visitante. El cron de limpieza que trae por defecto solo elimina 1.000 sesiones caducadas cada 48 horas. Si tienes 10.000 visitantes al día, estás acumulando sesiones muertas mucho más rápido de lo que se eliminan. He visto tiendas con más de 500.000 filas en esta tabla.
Y hay más: los datos de sesión están serializados en PHP, así que algunas filas son enormes. La tabla se fragmenta, las consultas se vuelven más lentas y, al final, el checkout empieza a fallar por timeout.
Límpiala manualmente y luego plantéate reducir el tiempo de expiración de las sesiones o ejecutar la limpieza con más frecuencia.
El Action Scheduler de WooCommerce mantiene las acciones completadas durante 30 días por defecto. Suena razonable hasta que compruebas el tamaño real de la tabla:
SELECT COUNT(*) FROM wp_actionscheduler_actions;
En una tienda con mucho tráfico procesando pedidos, enviando correos, sincronizando inventario… esta tabla puede alcanzar millones de filas. Otras consultas dependen de ella, los índices se vuelven pesados y la página de administración de “Acciones programadas” acaba siendo inutilizable.
Se puede solucionar con un filtro:
add_filter( 'action_scheduler_retention_period', function() {
return DAY_IN_SECONDS;
}); Mantén las acciones completadas solo durante 1 día en lugar de 30. No necesitas un mes de registros de “correo enviado correctamente”.
Cada consulta de WooCommerce que filtra por precio, SKU, estado de stock o cualquier campo personalizado realiza un escaneo completo de la tabla wp_postmeta, porque WordPress no añade un índice en meta_value. En una tienda con más de 500.000 filas en esa tabla, una sola búsqueda de productos puede tardar 2 segundos.
Una consulta lo soluciona:
ALTER TABLE wp_postmeta ADD INDEX idx_meta_value(meta_value(191));
Ese 191 es la longitud máxima para un índice en utf8mb4 en InnoDB. Después de esto, la misma búsqueda de productos puede bajar a unos 20 ms.
No está claro por qué WordPress core todavía no incluye este índice; es un problema conocido desde hace años.
Si tus páginas de WooCommerce Analytics van muy lentas, probablemente no es PHP, sino MySQL creando tablas temporales en disco. WooCommerce Analytics ejecuta consultas con GROUP BY sobre pedidos. Cuando el conjunto de resultados no cabe en memoria, MySQL escribe tablas temporales en disco como tablas MyISAM. El valor por defecto de tmp_table_size es 16 MB. Para una tienda con más de 10.000 pedidos, no es suficiente.
SHOW STATUS LIKE 'Created_tmp_disk_tables';
Si ese número sigue aumentando, configura:
tmp_table_size = 64M
max_heap_table_size = 64M
Ambos deben coincidir: MySQL usa el valor más bajo de los dos.
Después de esto, las consultas de analítica se mantienen en memoria en lugar de saturar el disco.
Los valores por defecto de PHP-FPM están pensados para un consumo mínimo de recursos, no para WooCommerce. La mayor parte del rendimiento que estás desaprovechando en el servidor está aquí.
Hay una herramienta de profiling gratuita integrada en tu servidor que la mayoría de equipos DevOps nunca activa. En la configuración del pool de PHP-FPM, añade:
slowlog = /var/log/php-fpm-slow.log
request_slowlog_timeout = 3s
Ahora, cualquier petición que tarde más de 3 segundos generará un stack trace completo en ese archivo de log. Verás exactamente qué función, qué plugin o qué hook de WooCommerce está bloqueando la ejecución. Sin herramientas externas, sin servicios de pago, sin cambios en el código.
Ha estado ahí todo el tiempo. Es lo primero que reviso en cualquier tienda WooCommerce lenta.
PHP tiene una opción en OPcache llamada opcache.interned_strings_buffer. El valor por defecto es 8 MB. WordPress + WooCommerce + plugins suelen necesitar 32–64 MB.
Cuando este valor es demasiado bajo, PHP no puede compartir datos de cadenas entre los procesos FPM. Como resultado, cada worker duplica su almacenamiento de cadenas en memoria. Si tienes 12 workers, eso supone 12× de uso de memoria para strings. El servidor parece que necesita más RAM, pero en realidad no es así: lo que necesita es este cambio en php.ini:
opcache.interned_strings_buffer=64
Puedes comprobar el uso actual con:
php -r "print_r(opcache_get_status());"
Fíjate en la sección interned_strings_usage. Si used_memory está cerca de buffer_size, estás desperdiciando RAM por duplicación de cadenas.
La mayoría de la gente deja pm.max_children en el valor por defecto (5) o lo sube a algo “optimista” como 50. Ambas opciones están mal.
La fórmula real es:
(Total RAM - sistema operativo - MySQL - Redis) / memoria media por proceso PHP
Para medir el tamaño real de un worker:
ps --no-headers -o rss -C php-fpm | awk '{sum+=$1} END {print sum/NR/1024"MB"}' Un worker típico de WooCommerce usa entre 40 y 80 MB. En un VPS de 2 GB, después de sistema operativo, MySQL y Redis, te quedan quizá 800 MB para PHP — eso son unos 10–20 workers.
Si lo pones en 50, tendrás OOM kills en momentos de checkout. Si lo dejas en 5, tendrás 45 clientes esperando en cola.
Es un cálculo básico que lleva 2 minutos, pero casi nadie lo hace.
Tu página carga rápido. El TTFB parece bueno. Pero el servidor se siente saturado con mucho menos tráfico del que debería soportar. Revisa qué se está ejecutando en los shutdown hooks de PHP.
Plugins que envían analíticas, llamadas a APIs o notificaciones de webhooks dentro de register_shutdown_function() mantienen el worker de FPM ocupado después de que la respuesta ya se ha enviado.
El usuario ve una página rápida, pero el worker no queda libre para la siguiente petición todavía. Si tienes 10 workers y cada uno queda bloqueado 500 ms por estos shutdown hooks, tu capacidad efectiva se reduce a la mitad.
El slow log de PHP-FPM también detecta estos casos.
PHP-FPM tiene tres modos de gestión de procesos: static, dynamic y ondemand. Para WooCommerce, nunca uses dynamic.
El modo dynamic intenta escalar workers cuando hace falta, pero ese escalado tiene latencia: crear nuevos procesos PHP cuesta tiempo. Durante un pico de checkout, los clientes esperan mientras PHP-FPM crea workers nuevos.
Para tiendas con mucho tráfico: usa pm = static. Los workers están siempre activos, sin coste de fork, y el comportamiento es predecible en memoria.
Para tráfico con picos: usa pm = ondemand. Los workers escalan a cero entre ráfagas y se crean bajo demanda. Al menos este modo asume el “arranque en frío”.
El modo dynamic intenta ser inteligente en ambos escenarios y falla en los dos. Es mejor elegir uno claramente.
A mí me gusta más Lite Speed que nginx, de hecho es lo que ofrece el hosting al que estoy llevando a todos mis clientes Lucus Host. Pero hay muchos usuarios de nginx así que incluyo también estos consejos.
La mayoría de quejas de “la caché no funciona” que he visto vienen de Nginx mal configurado, no de que esté roto. La configuración suele ser correcta, pero la estrategia no.
Si estás usando Nginx con fastcgi_cache, revisa tus reglas de bypass de caché. WooCommerce establece las cookies woocommerce_items_in_cart y woocommerce_cart_hash en cuanto alguien añade algo al carrito. Tu regla de exclusión tiene que comprobar específicamente estas cookies, no solo la de usuario logueado.
Si no lo haces, puedes acabar sirviendo páginas de carrito cacheadas con datos obsoletos. El cliente añade 3 artículos, ve 1. Y cuando paga, el total no coincide.
if ($cookie_woocommerce_items_in_cart) { set $skip_cache 1; } He depurado exactamente este problema en al menos una docena de tiendas. La caché “funciona”, pero sirve datos incorrectos a los usuarios.
La mayoría de configuraciones de Nginx abren una conexión nueva hacia PHP-FPM por cada petición. Incluso usando unix sockets, eso implica un ciclo de conexión/aceptación/cierre en cada request: entre 5 y 15 ms de sobrecarga por petición.
Puedes añadir esto en el bloque upstream:
upstream php-fpm {
server unix:/var/run/php-fpm.sock;
keepalive 16;
} Y en el bloque location:
proxy_http_version 1.1;
proxy_set_header Connection "";
Con esto, las conexiones se mantienen vivas entre peticiones. Los workers de PHP-FPM evitan el coste de aceptar y cerrar conexiones continuamente. En una tienda con carga alta (50+ requests/segundo), esto se traduce en un ahorro real.
Y esto es aún más importante con HTTP/2: los usuarios tienen multiplexación y compresión de cabeceras en el frontend, pero entre Nginx y PHP-FPM sigues en HTTP/1.1 si no hay keepalive. Sin ello, cada petición al backend abre una conexión nueva. Toda la optimización del frontend pierde parte de su efecto si el cuello de botella sigue en el backend.
Si usas Nginx con fastcgi_cache y el plugin nginx-helper para purgar, revisa cómo tienes configurado el purge. El comportamiento por defecto suele ser demasiado agresivo: purga toda la caché cuando se actualiza cualquier entrada.
Un cliente compra un producto. Cambia el stock. WooCommerce dispara una actualización del post. nginx-helper purga toda la caché. Todas las páginas del sitio quedan “en frío”. Los siguientes 50 visitantes golpean directamente PHP-FPM.
Eso no es caché. Es una bomba de tiempo activada por cada venta.
Deberías limitar la purga únicamente a la URL modificada y, como mucho, a la página de inicio. Un cambio de stock en un producto no debería eliminar la caché de otros cientos de páginas de producto.
He visto tiendas con un cache hit ratio por debajo del 20% por este motivo. Pensaban que la caché de Nginx “no funcionaba”, cuando en realidad estaba siendo destruida constantemente por la estrategia de purgado.
Cuando la caché de Nginx expira en una página de producto muy popular, sin stale-while-revalidate, el primer visitante después de la expiración espera una respuesta nueva de PHP. Y el segundo. Y el tercero. Todos golpeando PHP-FPM al mismo tiempo. Eso es un cache stampede una estampida de caché.
Se soluciona con dos líneas:
fastcgi_cache_use_stale updating;
fastcgi_cache_background_update on;
Ahora, cuando la caché expira, el primer visitante recibe la respuesta “caducada” (pero rápida). Nginx genera la versión nueva en segundo plano. Sin estampida, sin pico de caché en frío, sin timeouts en el checkout durante una venta.
Una página de producto típica de WooCommerce carga entre 15 y 30 archivos CSS y JS. Cada uno dispara una llamada al sistema open() en Nginx. Si lo multiplicas por las peticiones por segundo, eso se convierte en miles de accesos innecesarios al sistema de archivos.
Añade esto a tu configuración de Nginx:
open_file_cache max=10000 inactive=60s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
Ahora Nginx cachea descriptores de archivos y metadatos en memoria. La segunda petición a un mismo archivo evita completamente el acceso al sistema de archivos. En tiendas con muchas imágenes de variaciones de producto, esto tiene aún más impacto.
WordPress almacena todas las opciones con autoload en una sola clave de caché de objeto llamada alloptions. Es eficiente… hasta que un plugin llama a update_option() sobre cualquier opción con autoload.
Esa única llamada invalida toda la caché de alloptions. En la siguiente petición, WordPress vuelve a cargar todas las opciones autoload desde MySQL. Si tienes 2 MB de datos autoload, eso significa una consulta de 2 MB en el siguiente page load.
Los peores culpables suelen ser:
wp_optionsPuedes detectarlo con:
define('SAVEQUERIES', true); Después busca en el log de consultas:
UPDATE.*wp_options
Si algo está actualizando opciones en cada petición, ahí tienes el problema.
Cálculo rápido para dimensionar Redis en WooCommerce:
wp_alloptions: entre 500 KB y 2 MB por sí soloLa mayoría de gente asigna 64 MB a Redis y luego se pregunta por qué todo se ralentiza de forma aleatoria. Redis llega al límite, empieza a expulsar claves (evictions) y aparecen cache stampedes en las claves eliminadas.
Lo recomendable es poner maxmemory al menos al doble del uso típico real.
Compruébalo con:
redis-cli INFO memory | grep used_memory_human
Si estás por encima del 80% de maxmemory, estás en una zona peligrosa.
Por defecto, WordPress dispara una petición HTTP de loopback a su propio sitio en cada carga de página. Eso lo hace spawn_cron(), que ejecuta una comprobación completa con resolución DNS y handshake SSL a tu propio dominio, solo para ver si hay tareas programadas que ejecutar.
En hosting compartido, eso puede añadir entre 1 y 3 segundos de sobrecarga invisible. En cada petición.
La solución se implementa en dos minutos:
// wp-config.php
define('DISABLE_WP_CRON', true); Y luego un cron de sistema:
*/5 * * * * wget -q -O - https://yoursite.com/wp-cron.php
Así el cron se ejecuta cada 5 minutos desde un programador real del sistema, en lugar de ejecutarse en cada carga de página del usuario.
Si tu administración de WordPress va lenta —especialmente la lista de entradas/productos y las búsquedas— revisa los filtros pre_get_posts.
Un solo plugin que añada un meta_query sin comprobar is_admin() o is_main_query() puede hacer que todas las tablas del admin, todas las búsquedas AJAX y todas las consultas de productos acaben haciendo un JOIN no indexado contra wp_postmeta. En una tienda con 50.000 productos, eso significa un escaneo completo de tabla en cada tecla que escribes en la búsqueda del admin.
Puedes activar:
define('SAVEQUERIES', true); Y revisar las consultas en cualquier página lenta del admin. Casi siempre encontrarás al culpable ahí. Suele ser un plugin intentando ser “inteligente” con ordenaciones o filtros personalizados.
WordPress usa peticiones loopback para más cosas que el cron. El chequeo de Salud del Sitio las usa. El editor de bloques también. Las comprobaciones de actualizaciones de plugins las usan. Y WooCommerce para procesos en segundo plano.
Cada una de estas es tu servidor haciendo una petición HTTP a sí mismo a través de toda la pila de red: resolución DNS, conexión TCP, handshake SSL y una carga completa de WordPress en el lado receptor.
Si el DNS de tu servidor es lento, estás detrás de un CDN con reglas de firewall estrictas o tu hosting bloquea peticiones loopback, estas llamadas pueden fallar silenciosamente o añadir varios segundos de sobrecarga.
Revisa el informe de Salud del sitio para advertencias de “loopback request”. No son solo informativas: indican que algo no está funcionando bien en la comunicación del servidor consigo mismo.
Juntando todo esto: SAVEQUERIES te permite detectar culpables conocidos como el problema del filtro pre_get_posts y la invalidación de alloptions. Pero hay un patrón más profundo que merece la pena entender.
Cada plugin que escribe en wp_options durante una carga de página puede estar destruyendo tu cache hit ratio. Una sola llamada a update_option() en una ruta de código poco auditada, ejecutándose en cada petición, se multiplica con el tráfico.
Activa SAVEQUERIES, busca consultas UPDATE e INSERT sobre wp_options, y rastrea cada una hasta su plugin correspondiente. Los culpables siempre sorprenden.
Por qué falla Bizum WooCommerce: detecta errores de credenciales, firma, entorno y pedidos para recuperar…
Elige un plugin gratuito Redsys WordPress con criterio: compatibilidad, seguridad, configuración bancaria y límites para…
¿Qué necesito para activar CECA Online? Revisa contrato bancario, datos del terminal, seguridad, pruebas y…
Aprende a elegir una pasarela para suscripciones en WordPress: revisa recurrencia, compatibilidad, seguridad y soporte…
Elige un plugin Bizum WordPress compatible con tu flujo de venta, configura la pasarela bancaria…
Aprende cómo aceptar CECA en reservas WordPress, configurar pagos seguros y confirmar cada reserva sin…