Una tienda WooCommerce recién instalada funciona, pero cada visita ejecuta PHP y lanza decenas de consultas a la base de datos, y eso se nota cuando crecen el catálogo o el tráfico. En este tutorial optimizarás una tienda WooCommerce que ya funciona sobre Nginx, PHP 8.3-FPM y MariaDB en Ubuntu 24.04: ajustarás OPcache y PHP-FPM, darás memoria a MariaDB, añadirás caché de objetos con Redis y una caché de página completa con FastCGI de Nginx que no rompe el carrito ni el checkout. Medirás el tiempo de respuesta antes y después para comprobar la mejora.
Requisitos previos
Para seguir esta guía necesitas:
- Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con al menos 2 GB de RAM (4 GB o más para catálogos grandes).
- Un usuario no root con privilegios
sudo. - WordPress con WooCommerce funcionando sobre Nginx, PHP 8.3-FPM y MariaDB, con HTTPS activo. En la guía, el sitio está en
/var/www/your_domainy se sirve enyour_domain: sustituye ambos por tus valores. - WP-CLI instalado en
/usr/local/bin/wp. - Una copia de seguridad reciente de los archivos y la base de datos.
Todos los comandos wp se ejecutan como www-data, el usuario de PHP-FPM, para no dejar archivos con otro propietario.
Paso 1: Medir el punto de partida
Antes de cambiar nada, mide el tiempo hasta el primer byte (TTFB) de la portada y de una página de producto. Ejecuta cada medición varias veces y quédate con un valor típico:
curl -o /dev/null -s -w 'TTFB: %{time_starttransfer}s Total: %{time_total}s\n' https://your_domain/
curl -o /dev/null -s -w 'TTFB: %{time_starttransfer}s Total: %{time_total}s\n' https://your_domain/producto/ejemplo/
TTFB: 0.842s Total: 0.861s
TTFB: 0.915s Total: 0.934s
Apunta estos valores. Sin caché de página, un TTFB por encima de 500 ms en un VPS con recursos libres suele indicar que PHP o la base de datos son el cuello de botella.
Paso 2: Ajustar OPcache y los límites de PHP
OPcache guarda en memoria el código PHP ya compilado. WordPress con WooCommerce y varios plugins carga miles de archivos, y los valores por defecto se quedan cortos. Crea un archivo de configuración propio para PHP-FPM, así no tocas el php.ini del paquete:
sudo nano /etc/php/8.3/fpm/conf.d/99-woocommerce.ini
memory_limit = 256M
max_execution_time = 120
realpath_cache_size = 4096K
realpath_cache_ttl = 600
opcache.enable = 1
opcache.memory_consumption = 256
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 20000
opcache.validate_timestamps = 1
opcache.revalidate_freq = 60
Con opcache.revalidate_freq = 60, PHP comprueba cada minuto si los archivos han cambiado, así que las actualizaciones de plugins se aplican solas sin reiniciar el servicio.
Reinicia PHP-FPM y comprueba que los valores se han cargado:
sudo systemctl restart php8.3-fpm
sudo php-fpm8.3 -i | grep -E '^(memory_limit|opcache.memory_consumption|opcache.max_accelerated_files)'
memory_limit => 256M => 256M
opcache.max_accelerated_files => 20000 => 20000
opcache.memory_consumption => 256 => 256
Paso 3: Dimensionar el pool de PHP-FPM
El parámetro clave es pm.max_children: el número máximo de procesos PHP simultáneos. Si es demasiado bajo, las peticiones hacen cola; si es demasiado alto, el servidor se queda sin memoria y empieza a usar swap. Calcula cuánto ocupa cada proceso con la tienda en uso (navega un poco por ella antes):
ps --no-headers -o rss -C php-fpm8.3 | awk '{sum+=$1} END {printf "Procesos: %d Media: %.0f MB\n", NR, sum/NR/1024}'
Procesos: 6 Media: 78 MB
Aplica esta fórmula: memoria disponible para PHP dividida por la media por proceso. En un VPS de 4 GB en el que MariaDB, Redis y el sistema necesitan unos 1,5 GB, quedan unos 2,5 GB para PHP: 2.500 / 80 ≈ 30 procesos.
Edita el pool por defecto:
sudo nano /etc/php/8.3/fpm/pool.d/www.conf
Localiza estas directivas y ajústalas con tu cálculo. pm.max_requests recicla cada proceso tras 500 peticiones para contener fugas de memoria de plugins, y el slowlog registra las peticiones que tardan más de 5 segundos junto con la traza de PHP, lo que te dirá qué plugin es lento:
pm = dynamic
pm.max_children = 30
pm.start_servers = 6
pm.min_spare_servers = 4
pm.max_spare_servers = 10
pm.max_requests = 500
request_slowlog_timeout = 5s
slowlog = /var/log/php8.3-fpm-slow.log
Comprueba la sintaxis y reinicia el servicio:
sudo php-fpm8.3 -t
sudo systemctl restart php8.3-fpm
NOTICE: configuration file /etc/php/8.3/fpm/php-fpm.conf test is successful
Si en el registro de PHP-FPM aparece el aviso server reached pm.max_children setting, aumenta el valor siempre que la memoria lo permita:
sudo grep max_children /var/log/php8.3-fpm.log
Paso 4: Optimizar MariaDB
El parámetro con más impacto en MariaDB es innodb_buffer_pool_size, la memoria donde InnoDB mantiene tablas e índices. Lo ideal es que toda la base de datos quepa en él. Consulta primero su tamaño (sustituye wordpress por el nombre de tu base de datos):
sudo mariadb -e "SELECT table_schema AS db, ROUND(SUM(data_length + index_length)/1024/1024) AS mb FROM information_schema.tables WHERE table_schema = 'wordpress' GROUP BY table_schema;"
+-----------+------+
| db | mb |
+-----------+------+
| wordpress | 640 |
+-----------+------+
Crea un archivo de configuración propio. En un VPS de 4 GB que comparte servidor con PHP, 1 GB de buffer pool es un buen punto de partida; no pases del 40 % de la RAM total en ese caso:
sudo nano /etc/mysql/mariadb.conf.d/99-woocommerce.cnf
[mysqld]
innodb_buffer_pool_size = 1G
innodb_log_file_size = 256M
innodb_flush_log_at_trx_commit = 2
tmp_table_size = 64M
max_heap_table_size = 64M
slow_query_log = 1
slow_query_log_file = /var/log/mysql/mariadb-slow.log
long_query_time = 1
innodb_flush_log_at_trx_commit = 2 escribe el registro de transacciones a disco una vez por segundo en lugar de en cada commit. Mejora mucho el rendimiento de escritura a cambio de poder perder como máximo un segundo de transacciones si se cae el servidor; si no aceptas ese riesgo, elimina la línea. El registro de consultas lentas te mostrará las que superen un segundo.
Reinicia MariaDB y comprueba el valor:
sudo systemctl restart mariadb
sudo mariadb -e "SELECT @@innodb_buffer_pool_size/1024/1024 AS buffer_pool_mb;"
+----------------+
| buffer_pool_mb |
+----------------+
| 1024.00000000 |
+----------------+
Revisa también cuántos datos carga WordPress en cada petición desde la tabla de opciones (ajusta el prefijo wp_ si usas otro). Por encima de 1 MB conviene investigar qué plugin guarda opciones tan grandes:
sudo mariadb wordpress -e "SELECT ROUND(SUM(LENGTH(option_value))/1024) AS autoload_kb FROM wp_options WHERE autoload IN ('yes','on','auto-on','auto');"
Paso 5: Activar el almacenamiento de pedidos de alto rendimiento
WooCommerce guardaba históricamente los pedidos en las tablas genéricas de entradas de WordPress. El almacenamiento de pedidos de alto rendimiento (HPOS) usa tablas propias con índices adecuados y acelera tanto el checkout como el listado de pedidos. Está activo por defecto en las tiendas creadas con WooCommerce 8.2 o posterior.
Si tu tienda es anterior, ve a WooCommerce > Ajustes > Avanzado > Funcionalidades, marca la sincronización de pedidos, espera a que termine y después selecciona Almacenamiento de pedidos de alto rendimiento. Antes, confirma en esa misma pantalla que todos tus plugins son compatibles; WooCommerce lista los que no lo son.
Paso 6: Añadir caché de objetos con Redis
WordPress repite muchas consultas idénticas en cada petición (opciones, términos, metadatos de productos). Una caché de objetos persistente las guarda en Redis y beneficia sobre todo a las páginas que no pueden cachearse enteras: carrito, checkout, Mi cuenta y el panel de administración.
Instala Redis y la extensión de PHP:
sudo apt install -y redis-server php8.3-redis
Limita la memoria de Redis y haz que descarte las claves menos usadas al llenarse, el comportamiento adecuado para una caché:
sudo nano /etc/redis/redis.conf
Busca y ajusta estas directivas:
maxmemory 256mb
maxmemory-policy allkeys-lru
Reinicia Redis y PHP-FPM y comprueba que Redis responde:
sudo systemctl restart redis-server php8.3-fpm
redis-cli ping
PONG
Instala y activa el plugin Redis Object Cache con WP-CLI:
cd /var/www/your_domain
sudo -u www-data wp plugin install redis-cache --activate
Abre wp-config.php y añade estas líneas justo antes de /* That's all, stop editing! */. El prefijo evita colisiones si alojas más de un WordPress en el mismo Redis:
sudo nano /var/www/your_domain/wp-config.php
define( 'WP_REDIS_HOST', '127.0.0.1' );
define( 'WP_REDIS_PORT', 6379 );
define( 'WP_REDIS_PREFIX', 'your_domain:' );
Activa el drop-in de caché y comprueba el estado:
sudo -u www-data wp redis enable
sudo -u www-data wp redis status
Status: Connected
Client: PhpRedis
Drop-in: Valid
...
Tras navegar por la tienda, redis-cli info keyspace debería mostrar claves en db0.
Paso 7: Configurar la caché FastCGI de Nginx
La caché FastCGI guarda en disco el HTML generado por PHP y lo sirve directamente desde Nginx a los visitantes anónimos, sin ejecutar WordPress. En una tienda hay que excluir todo lo personal: carrito, checkout, Mi cuenta, peticiones POST y los visitantes que tienen productos en el carrito o sesión iniciada.
Define la zona de caché en el contexto http, en un archivo de conf.d:
sudo nano /etc/nginx/conf.d/fastcgi-cache.conf
fastcgi_cache_path /var/cache/nginx/woocommerce levels=1:2 keys_zone=WOOCOMMERCE:100m max_size=1g inactive=60m;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_cache_use_stale error timeout updating http_500 http_503;
Ahora edita el bloque de servidor de tu tienda:
sudo nano /etc/nginx/sites-available/your_domain
Dentro del bloque server que escucha en el puerto 443, antes de los bloques location, añade las reglas de exclusión. Incluye las rutas en inglés y las traducidas, porque dependen del idioma con el que se crearon las páginas de WooCommerce; comprueba las tuyas en WooCommerce > Ajustes > Avanzado:
set $skip_cache 0;
if ($request_method = POST) {
set $skip_cache 1;
}
if ($query_string != "") {
set $skip_cache 1;
}
if ($request_uri ~* "/wp-admin/|/wp-login.php|/xmlrpc.php|/wp-json/|/feed/|sitemap(_index)?.xml|/cart/|/checkout/|/my-account/|/carrito/|/finalizar-compra/|/mi-cuenta/") {
set $skip_cache 1;
}
if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_logged_in|woocommerce_items_in_cart|woocommerce_cart_hash|wp_woocommerce_session_") {
set $skip_cache 1;
}
En el bloque location ~ \.php$ que ya tienes, añade las directivas de caché después de fastcgi_pass:
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_cache WOOCOMMERCE;
fastcgi_cache_valid 200 301 302 10m;
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
add_header X-FastCGI-Cache $upstream_cache_status;
}
Notaen Nginx, un
add_headerdentro de unlocationanula todos losadd_headerdefinidos a nivel deserver. Si en elservertienes cabeceras de seguridad (por ejemploStrict-Transport-Security), repítelas dentro de este bloque y del de archivos estáticos del paso 9.
Una validez de 10 minutos limita el tiempo que un cambio de precio o de stock tarda en verse en las páginas cacheadas. El carrito y el checkout siempre se calculan en PHP, así que el cliente paga siempre el precio correcto.
Crea el directorio de caché, comprueba la configuración y recarga Nginx:
sudo mkdir -p /var/cache/nginx/woocommerce
sudo chown www-data:www-data /var/cache/nginx/woocommerce
sudo nginx -t
sudo systemctl reload nginx
Comprueba el comportamiento con dos peticiones seguidas a la portada. La primera genera la caché y la segunda debe servirse desde ella:
curl -sI https://your_domain/ | grep -i x-fastcgi-cache
curl -sI https://your_domain/ | grep -i x-fastcgi-cache
x-fastcgi-cache: MISS
x-fastcgi-cache: HIT
El carrito nunca debe cachearse:
curl -sI https://your_domain/carrito/ | grep -i x-fastcgi-cache
x-fastcgi-cache: BYPASS
Haz también una prueba real en una ventana de incógnito: añade un producto al carrito, navega a otra página y confirma que el contador del carrito se mantiene, y completa un pedido de prueba. Si después de publicar un cambio necesitas vaciar la caché al momento, borra su contenido con sudo find /var/cache/nginx/woocommerce -type f -delete.
Paso 8: Mover WP-Cron al cron del sistema
Por defecto, WordPress ejecuta sus tareas programadas (y WooCommerce su Action Scheduler: correos, webhooks, pedidos pendientes) aprovechando las visitas de los clientes, lo que ralentiza algunas peticiones. Con la caché de página además apenas llegan visitas a PHP, así que las tareas se retrasan. Desactiva ese mecanismo en wp-config.php, junto a las líneas de Redis:
define( 'DISABLE_WP_CRON', true );
Programa la ejecución cada 5 minutos en el crontab de www-data:
sudo crontab -u www-data -e
*/5 * * * * cd /var/www/your_domain && /usr/local/bin/wp cron event run --due-now --quiet
Comprueba que el comando funciona ejecutándolo a mano:
cd /var/www/your_domain
sudo -u www-data wp cron event run --due-now
Executed the cron event 'action_scheduler_run_queue' in 0.412s.
Success: Executed a total of 3 cron events.
En WooCommerce > Estado > Acciones programadas, la pestaña Pendientes no debería acumular acciones vencidas.
Paso 9: Comprimir y cachear los archivos estáticos
Deja que el navegador guarde CSS, JavaScript, imágenes y fuentes. Añade este bloque dentro del server de tu tienda:
location ~* \.(css|js|jpg|jpeg|png|gif|webp|avif|svg|ico|woff2?)$ {
expires 30d;
add_header Cache-Control "public";
access_log off;
try_files $uri =404;
}
Activa la compresión gzip para texto. En /etc/nginx/nginx.conf, dentro de http, gzip on; ya viene activado; descomenta y ajusta las líneas de gzip que trae el archivo para que queden así:
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 5;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml;
Comprueba y recarga Nginx, y verifica la compresión:
sudo nginx -t && sudo systemctl reload nginx
curl -sI -H 'Accept-Encoding: gzip' https://your_domain/ | grep -i content-encoding
content-encoding: gzip
Para las imágenes de producto, que suelen ser el mayor peso de la página, súbelas ya redimensionadas o usa un plugin que genere versiones WebP o AVIF.
Paso 10: Medir de nuevo
Repite las mediciones del paso 1 como visitante anónimo:
curl -o /dev/null -s -w 'TTFB: %{time_starttransfer}s Total: %{time_total}s\n' https://your_domain/
curl -o /dev/null -s -w 'TTFB: %{time_starttransfer}s Total: %{time_total}s\n' https://your_domain/producto/ejemplo/
TTFB: 0.041s Total: 0.048s
TTFB: 0.038s Total: 0.045s
Las páginas servidas desde la caché FastCGI deberían responder en decenas de milisegundos. Para medir las páginas dinámicas, abre el carrito con sesión iniciada y compara el tiempo con el de antes de activar Redis.
Solución de problemas
El carrito se vacía o muestra productos de otro cliente. Alguna página personal se está cacheando. Comprueba la cabecera X-FastCGI-Cache de esa URL, añade su ruta a la expresión $request_uri y vacía la caché.
wp redis status muestra Not connected. Comprueba que Redis está en marcha con systemctl status redis-server y que la extensión está cargada con php-fpm8.3 -m | grep redis.
Errores 502 tras cambiar el pool. Revisa sudo journalctl -u php8.3-fpm y /var/log/php8.3-fpm.log. Si hay procesos terminados por falta de memoria (dmesg | grep -i oom), baja pm.max_children.
MariaDB no arranca tras el cambio. Revisa sudo journalctl -u mariadb. Normalmente el innodb_buffer_pool_size es mayor que la memoria libre: redúcelo.
Conclusión
Tu tienda WooCommerce sirve ahora las páginas públicas desde la caché de Nginx, usa Redis para las páginas dinámicas, tiene PHP-FPM y MariaDB dimensionados para tu servidor y ejecuta sus tareas programadas desde el cron del sistema. Como siguientes pasos, revisa cada semana el slowlog de PHP-FPM y el registro de consultas lentas de MariaDB para detectar plugins problemáticos, pon un CDN delante de los archivos estáticos si vendes a varios países y prueba la tienda con carga antes de campañas como el Black Friday.
