--- title: "Optimizar Core Web Vitals desde el servidor con Nginx y PHP" description: "Reduce el TTFB y mejora el LCP desde el servidor en Ubuntu 24.04 con OPcache, ajuste de PHP-FPM, caché FastCGI de Nginx, compresión Brotli y caché de estáticos." category: "Rendimiento Web" language: es source: https://cubepath.com/es/docs/rendimiento-web/optimizacion-de-web-vitals-del-lado-del-servidor updated: 2026-09-25 --- # Optimizar Core Web Vitals desde el servidor con Nginx y PHP Los Core Web Vitals son las métricas con las que Google mide la experiencia de carga de una página: LCP (Largest Contentful Paint), INP (Interaction to Next Paint) y CLS (Cumulative Layout Shift). Aunque gran parte se decide en el frontend, el servidor marca el punto de partida: nada se pinta hasta que llega el primer byte, así que un TTFB alto arrastra directamente el LCP. En este tutorial medirás el TTFB de un sitio PHP servido con Nginx en Ubuntu 24.04 y lo reducirás con OPcache, un pool de PHP-FPM bien dimensionado, la caché FastCGI de Nginx, compresión Brotli y cabeceras de caché para los estáticos. ## Requisitos previos - Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath. - Un usuario no root con privilegios `sudo`. - Nginx y PHP 8.3 con PHP-FPM (`php8.3-fpm`, el de Ubuntu 24.04) sirviendo una aplicación en `tudominio.com`, por ejemplo WordPress. - HTTPS configurado en el sitio. - `jq` para leer respuestas JSON: `sudo apt install jq`. ## Qué puede mejorar el servidor | Métrica | Umbral "bueno" | Palanca en el servidor | |---------|----------------|------------------------| | TTFB | 800 ms o menos | OPcache, PHP-FPM, caché de página | | LCP | 2,5 s o menos | TTFB bajo, compresión, caché de estáticos, imágenes ligeras | | INP | 200 ms o menos | Casi todo en el cliente (JavaScript) | | CLS | 0,1 o menos | Casi todo en el cliente (dimensiones de imágenes, fuentes) | Esta guía se centra en TTFB y LCP, que es donde la configuración del servidor tiene efecto directo. ## Paso 1: Medir el punto de partida Mide desde otra máquina, no desde el propio servidor, para incluir la latencia de red. `curl` desglosa cada fase de la petición: ```bash curl -o /dev/null -s -w "DNS: %{time_namelookup}s\nTCP: %{time_connect}s\nTLS: %{time_appconnect}s\nTTFB: %{time_starttransfer}s\nTotal: %{time_total}s\n" https://tudominio.com/ ``` ```output DNS: 0.012s TCP: 0.041s TLS: 0.089s TTFB: 0.912s Total: 0.968s ``` La diferencia entre `TTFB` y `TLS` es el tiempo que el servidor tarda en generar la página (aquí, más de 800 ms). Ejecuta el comando varias veces y anota un valor típico. Obtén también las métricas de laboratorio de Lighthouse con la API pública de PageSpeed Insights: ```bash curl -s "https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=https://tudominio.com/&strategy=mobile" \ | jq '.lighthouseResult.audits | {ttfb: .["server-response-time"].displayValue, lcp: .["largest-contentful-paint"].displayValue, cls: .["cumulative-layout-shift"].displayValue}' ``` ```output { "ttfb": "Root document took 890 ms", "lcp": "3.8 s", "cls": "0.02" } ``` ## Paso 2: Ajustar OPcache OPcache guarda en memoria el código PHP ya compilado, de modo que no se vuelve a analizar en cada petición. Viene activado en Ubuntu, pero con valores pensados para sitios pequeños. Crea un archivo de ajustes que se cargue después del de por defecto: ```bash sudo nano /etc/php/8.3/fpm/conf.d/99-opcache-tuning.ini ``` ```ini 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 ``` - `memory_consumption` (MB) y `max_accelerated_files` deben ser suficientes para que quepa todo el código; WordPress con plugins supera fácilmente los 10.000 archivos. - `revalidate_freq=60` hace que PHP compruebe si los archivos cambiaron como mucho una vez por minuto. Si despliegas de forma controlada, puedes poner `opcache.validate_timestamps=0` y ejecutar `sudo systemctl reload php8.3-fpm` tras cada despliegue. Reinicia PHP-FPM y comprueba los valores: ```bash sudo systemctl restart php8.3-fpm php-fpm8.3 -i | grep -E 'opcache.memory_consumption|opcache.max_accelerated_files' ``` ```output opcache.max_accelerated_files => 20000 => 20000 opcache.memory_consumption => 256 => 256 ``` ## Paso 3: Dimensionar el pool de PHP-FPM Si todas las peticiones concurrentes superan el número de procesos PHP disponibles, las demás esperan en cola y el TTFB se dispara. Averigua cuánta memoria usa de media cada proceso: ```bash ps --no-headers -o rss -C php-fpm8.3 | awk '{sum+=$1; n++} END {printf "%.0f MB de media en %d procesos\n", sum/n/1024, n}' ``` ```output 62 MB de media en 6 procesos ``` Divide la memoria que puedes dedicar a PHP entre ese valor. Por ejemplo, en un servidor de 4 GB reservando 1,5 GB para el sistema, MySQL y Nginx: 2.500 MB / 62 MB = unos 40 procesos. Edita el pool: ```bash sudo nano /etc/php/8.3/fpm/pool.d/www.conf ``` Localiza y ajusta estas directivas: ```ini pm = dynamic pm.max_children = 40 pm.start_servers = 8 pm.min_spare_servers = 4 pm.max_spare_servers = 12 pm.max_requests = 500 request_slowlog_timeout = 2s slowlog = /var/log/php8.3-fpm-slow.log ``` `pm.max_requests` recicla cada proceso tras 500 peticiones para contener fugas de memoria, y el slowlog registra la traza de cualquier petición que tarde más de 2 segundos. Comprueba la sintaxis y recarga: ```bash sudo php-fpm8.3 -t sudo systemctl reload php8.3-fpm ``` ```output NOTICE: configuration file /etc/php/8.3/fpm/php-fpm.conf test is successful ``` Si en el log de PHP-FPM aparece `server reached pm.max_children setting`, el pool se queda corto: ```bash sudo grep max_children /var/log/php8.3-fpm.log ``` ## Paso 4: Activar la caché FastCGI de Nginx La mayor reducción de TTFB llega al servir las páginas públicas desde caché, sin ejecutar PHP. Nginx guarda la respuesta de PHP-FPM en disco y la entrega directamente a los siguientes visitantes. Define la zona de caché y las reglas de exclusión en el contexto `http`: ```bash sudo nano /etc/nginx/conf.d/fastcgi-cache.conf ``` ```nginx fastcgi_cache_path /var/cache/nginx/fastcgi levels=1:2 keys_zone=PAGES:50m max_size=1g inactive=60m use_temp_path=off; fastcgi_cache_key "$scheme$request_method$host$request_uri"; # No cachear usuarios con sesión iniciada map $http_cookie $skip_cache_cookie { default 0; "~*wordpress_logged_in|wp-postpass|comment_author|woocommerce_items_in_cart|PHPSESSID" 1; } # No cachear zonas privadas map $request_uri $skip_cache_uri { default 0; "~*^/(wp-admin|wp-login\.php|cart|checkout|my-account)" 1; } ``` Los patrones de ejemplo son los de WordPress y WooCommerce; adáptalos a las cookies de sesión y rutas privadas de tu aplicación. Ahora edita el bloque `location` de PHP en `/etc/nginx/sites-available/tudominio.com`: ```nginx location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php8.3-fpm.sock; set $skip_cache 0; if ($request_method = POST) { set $skip_cache 1; } if ($skip_cache_cookie) { set $skip_cache 1; } if ($skip_cache_uri) { set $skip_cache 1; } fastcgi_cache PAGES; fastcgi_cache_valid 200 301 10m; fastcgi_cache_bypass $skip_cache; fastcgi_no_cache $skip_cache; fastcgi_cache_use_stale error timeout updating http_500; fastcgi_cache_lock on; add_header X-Cache-Status $upstream_cache_status; } ``` - `fastcgi_cache_valid 200 301 10m` guarda las páginas 10 minutos; súbelo si tu contenido cambia poco. - `fastcgi_cache_use_stale` sirve la copia antigua si PHP falla o mientras se regenera, y `fastcgi_cache_lock` evita que diez visitas simultáneas generen la misma página diez veces. - Nginx no guarda en caché respuestas que incluyen `Set-Cookie`, así que las páginas que inician sesión quedan fuera automáticamente. Comprueba y recarga: ```bash sudo nginx -t sudo systemctl reload nginx ``` Pide la misma página dos veces y mira la cabecera de estado: ```bash curl -sI https://tudominio.com/ | grep -i x-cache-status curl -sI https://tudominio.com/ | grep -i x-cache-status ``` ```output x-cache-status: MISS x-cache-status: HIT ``` `MISS` significa que se generó con PHP y se guardó; `HIT`, que se sirvió desde caché; `BYPASS`, que se saltó por alguna regla de exclusión. Para vaciar la caché tras cambiar contenido, borra su contenido con `sudo find /var/cache/nginx/fastcgi -type f -delete`. ## Paso 5: Comprimir con Brotli Cuanto menos pesan el HTML, el CSS y el JavaScript, antes se descargan los recursos que bloquean el renderizado. Instala los módulos de Brotli de Ubuntu: ```bash sudo apt install libnginx-mod-http-brotli-filter libnginx-mod-http-brotli-static ``` Crea la configuración de compresión: ```bash sudo nano /etc/nginx/conf.d/compression.conf ``` ```nginx # "gzip on;" ya está en nginx.conf: no lo repitas aquí gzip_vary on; gzip_proxied any; gzip_comp_level 5; gzip_types text/plain text/css text/javascript application/javascript application/json application/xml image/svg+xml; brotli on; brotli_comp_level 5; brotli_types text/plain text/css text/javascript application/javascript application/json application/xml image/svg+xml; ``` Recarga y verifica: ```bash sudo nginx -t && sudo systemctl reload nginx curl -s -o /dev/null -D - -H "Accept-Encoding: br, gzip" https://tudominio.com/ | grep -i content-encoding ``` ```output content-encoding: br ``` ## Paso 6: Cachear los estáticos en el navegador En las visitas repetidas, el navegador no debería volver a pedir CSS, JavaScript, fuentes ni imágenes. Añade este bloque dentro del `server` de tu sitio, antes del `location` de PHP: ```nginx location ~* \.(css|js|mjs|woff2|svg|png|jpe?g|gif|webp|avif|ico)$ { add_header Cache-Control "public, max-age=31536000, immutable"; access_log off; try_files $uri =404; } ``` > Importante: `immutable` y un año de caché solo son seguros si el nombre del archivo cambia con cada versión (`app.3f9a1c.css` o `style.css?ver=6.6`, como hace WordPress). Si publicas siempre el mismo nombre, reduce la duración. Recarga Nginx y comprueba un recurso: ```bash sudo nginx -t && sudo systemctl reload nginx curl -sI https://tudominio.com/wp-includes/css/dist/block-library/style.min.css | grep -iE 'cache-control|expires' ``` ```output cache-control: public, max-age=31536000, immutable ``` ## Paso 7: Volver a medir Repite las mediciones del paso 1, primero con `curl`: ```bash curl -o /dev/null -s -w "TTFB: %{time_starttransfer}s\n" https://tudominio.com/ ``` ```output TTFB: 0.094s ``` Y después con PageSpeed Insights. Los datos de laboratorio cambian en el momento; los datos reales de usuarios (el bloque `loadingExperience` de la misma respuesta, procedente del Chrome UX Report) tardan hasta 28 días en reflejar la mejora. ## Solución de problemas **`x-cache-status` siempre muestra `BYPASS`.** Alguna cookie coincide con el mapa de exclusión. Revisa las cookies que envía tu navegador; si la aplicación crea `PHPSESSID` para todos los visitantes, la caché nunca se usará hasta que deje de hacerlo en páginas públicas. **La cabecera `x-cache-status` no aparece.** Otro `add_header` en un nivel superior no se hereda, o la petición no pasa por el `location` de PHP. Comprueba con `sudo nginx -T | grep -n fastcgi_cache` que la configuración cargada es la esperada. **El TTFB sigue alto con `HIT`.** El problema ya no es PHP: revisa la latencia de red con los tiempos `TCP` y `TLS` del paso 1 y considera activar HTTP/3 o servir el sitio desde una región más cercana a tus usuarios. **Peticiones lentas sin caché.** Consulta el slowlog configurado en el paso 3 para ver en qué función se pierde el tiempo: ```bash sudo tail -n 40 /var/log/php8.3-fpm-slow.log ``` ## Conclusión Has reducido el TTFB sirviendo las páginas públicas desde la caché FastCGI, has acelerado las que requieren PHP con OPcache y un pool bien dimensionado, y has aligerado y cacheado los recursos que determinan el LCP. Como siguientes pasos, optimiza las imágenes con versiones WebP y AVIF, activa HTTP/3 en Nginx y añade la medición de PageSpeed Insights a tu proceso de despliegue para detectar regresiones.