nginx LCP: transport, not content
The nginx-side LCP failures cluster around transport, not content. Three of them keep showing up: HTML on the slow path while images sit cacheable behind it, on-the-fly gzip serializing CPU per response, and HTTP/1.1 head-of-line blocking that queues the LCP image behind CSS. The fix is to get the transport layer out of the way (HTTP/2, pre-compressed assets, TLS resumption), then route image and CSS work through ModPageSpeed (the ngx_pagespeed module) so the same recipe applies regardless of what backend nginx is fronting.
What LCP measures
LCP — Largest Contentful Paint — is the render time of the largest above-the-fold element on a page. On most sites that is a hero image, video poster, or H1. Google considers anything under 2.5 s “good”; anything over 4 s is “poor”. The field measurement comes from Chrome’s CrUX dataset (real users over the last 28 days); the lab measurement comes from Lighthouse and PageSpeed Insights. The two often disagree because real users are slower than your developer machine. See web.dev/articles/lcp for the canonical definition.
The most common LCP failures on nginx
These three keep showing up on nginx-fronted sites, independent of the backend:
-
Origin is fast, but the static/dynamic split leaks. A typical
try_files $uri @app;pattern routes static files from disk and falls through to an upstream for dynamic content. The static side is fine — the dynamic side ships responses withoutCache-Control: s-maxage, so the CDN treats HTML as uncacheable and re-fetches it. LCP suffers because the HTML is on the slow path and the LCP image (cacheable) waits behind it. -
gzip on;is set butgzip_static off;is the default. nginx compresses on the fly on every request — 5–30 ms of CPU per response — instead of serving pre-compressed.gzfiles from disk. On a busy server, this serializes behind the worker pool and the LCP image’s first byte is delayed. -
HTTP/1.1 head-of-line blocking on a single connection. Sites still on HTTP/1.1 (no
listen 443 ssl http2;) push all assets through one or two parallel connections; the LCP image queues behind CSS and JS that the browser opens first. Even with the rewriter optimizing bytes, the transport bottleneck eats the win.
What the transport layer reveals
Before changing anything:
- Run PageSpeed Insights on the affected URL:
https://pagespeed.web.dev/analysis?url=<URL>. Read the Largest Contentful Paint element breakdown — the four phases (TTFB, resource load delay, resource load duration, element render delay) tell you which layer to fix. curl -sI -H 'Accept-Encoding: gzip, br' https://<host>/index.html— check forcontent-encoding: br(Brotli) orgzipon the document, andCache-Controlheaders. Missing compression on the document is failure mode #2; missings-maxageis failure mode #1.curl -sI --http2 -o /dev/null -w '%{http_version}\n' https://<host>/— should print2. If it prints1.1, HTTP/2 isn’t enabled (failure mode #3).- Chrome DevTools → Network panel → reload, look at the Waterfall column. HTTP/1.1 shows a clear “6-connection limit” stagger; HTTP/2 shows requests landing in parallel. The LCP image should be in the first wave of requests, not waiting on CSS/JS.
- For repeatable measurement across config changes: WebPageTest scripted runs on a fixed connection profile. WebPageTest’s “Connection View” diagnoses transport problems faster than Lighthouse.
Output you’re hunting for: protocol, compression, cache headers, and the LCP element’s timing breakdown. If TTFB is the long pole, the rewriter can’t help; fix the backend first. If resource load duration is the long pole, the rewriter is the right tool.
Step 2: Rewrite images and inline critical CSS with ngx_pagespeed
ModPageSpeed loads as an nginx module (ngx_pagespeed). It rewrites HTML on the way out and serves optimized image variants from a shared cache. nginx is the canonical deployment target, so this is the cleanest configuration in the matrix. The filters that move LCP here:
recompress_images+convert_jpeg_to_webp— re-encodes hero images at a configured quality and transcodes to WebP for browsers that advertise support.inline_preview_images— ships a sub-1 KB LQIP placeholder inline in the HTML for the hero, so the first paint shows something during the network wait.prioritize_critical_css— extracts above-the-fold rules and inlines them into the document<head>, defers the rest. Kills the multi-stylesheet<head>chain that backend-emitted HTML usually carries.hint_preload_subresources— emitsLink: rel=preloadresponse headers for resources the rewriter has identified as needed for above-the-fold rendering. The browser starts fetching them before the document body parses.lazyload_images— defers offscreen image loading; this is marked “Test first” in the filter reference and changes load order. Enable it for image-heavy pages, but keep the LCP image out of the lazy set — the rewriter’s beacon-based detection identifies above-the-fold images automatically; verify with?PageSpeedFilters=+debug.
Minimal nginx config:
pagespeed on;
pagespeed FileCachePath /var/cache/ngx_pagespeed;
pagespeed RewriteLevel CoreFilters;
pagespeed EnableFilters recompress_images;
pagespeed EnableFilters convert_jpeg_to_webp;
pagespeed EnableFilters inline_preview_images;
pagespeed EnableFilters prioritize_critical_css;
pagespeed EnableFilters hint_preload_subresources;
# Tune image recompression
pagespeed ImageRecompressionQuality 82;
Place these directives in the server { } block. The rewriter runs on responses returned through the location block, regardless of whether they come from a proxy_pass, fastcgi_pass, or try_files path.
After enabling, re-run PSI. LCP attribution should show the rewritten WebP variant being served and the resource load duration should drop. If LCP element render delay stays high, the long pole is the CSS chain or the transport — see the transport-layer tuning below.
Transport-layer tuning
What you do at the nginx layer to stack additional wins:
- Enable HTTP/2:
listen 443 ssl http2;in the server block. HTTP/3 if available — requires nginx ≥ 1.25 with QUIC compiled in. gzip_static on;plus ship pre-compressed.gzand.brfiles alongside the originals. Thengx_brotlimodule is widely packaged; enable both withbrotli_static on;.ssl_session_cache shared:SSL:10m;andssl_session_timeout 1d;— TLS resumption saves roughly 80 ms on repeat visits, especially on mobile networks where RTT dominates.- For dynamic responses, set
add_header Cache-Control "public, s-maxage=60, stale-while-revalidate=30";so the CDN can serve stale HTML while revalidating in the background. - Audit the loaded config:
nginx -T | grep -E 'gzip|http2|brotli|ssl_session'. The file you edit isn’t necessarily the file that’s loaded if there areincludeglob mismatches or a stale package config under/etc/nginx/conf.d/.
Confirming the transport win
curl -sI --http2 -H 'Accept: image/webp,*/*' https://<host>/<hero-url>— confirmcontent-type: image/webpandhttp/2 200. Confirmcache-controlis set as configured.- Open the page with
?PageSpeedFilters=+debug— the response includes HTML comments showing which filters ran on which assets. Confirm the hero is in the rewritten set and is not lazy-loaded. - Re-run PSI on the same URL. The LCP element timing should show smaller resource load duration (WebP + compression) and smaller resource load delay (HTTP/2 + preload).
- After 28 days, check Search Console → Core Web Vitals report. The URL group should move from “Needs improvement” to “Good”. Watch field data, not just lab.
The nginx snippet
# nginx — minimal config to address LCP, stack-agnostic
server {
listen 443 ssl http2;
server_name example.com;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
gzip on;
gzip_static on;
# ngx_brotli module:
brotli on;
brotli_static on;
pagespeed on;
pagespeed FileCachePath /var/cache/ngx_pagespeed;
pagespeed RewriteLevel CoreFilters;
pagespeed EnableFilters recompress_images;
pagespeed EnableFilters convert_jpeg_to_webp;
pagespeed EnableFilters inline_preview_images;
pagespeed EnableFilters prioritize_critical_css;
pagespeed EnableFilters hint_preload_subresources;
pagespeed ImageRecompressionQuality 82;
location / {
try_files $uri @app;
add_header Cache-Control "public, s-maxage=60, stale-while-revalidate=30";
}
}
The full directive reference lives in the filter reference. For the install steps on nginx (Debian, Ubuntu, RHEL/AlmaLinux), see installation as an nginx module.
When this doesn’t work
Cases where ModPageSpeed alone isn’t enough on nginx:
- TTFB is the long pole. If
curl -w '%{time_starttransfer}\n'shows over 1 s, no amount of HTML rewriting fixes LCP — the browser is already 1 s in when the first byte arrives. Fix the backend (PHP-FPM tuning, Node process count, database query plan) before the rewriter. - The LCP element is a third-party embed. Lazy-loaded YouTube iframes and CMP-injected video posters often become the LCP candidate. ModPageSpeed cannot rewrite third-party iframe contents. Use a facade pattern; the LCP becomes your own static image.
- The HTML is built by JavaScript on the client. Single-page apps (React, Vue, Svelte hydrating from a thin shell) construct the LCP element after page load. The nginx-layer rewriter doesn’t see the runtime DOM. SPA LCP work moves to build time (Next.js
<Image>, route-level code-splitting) or to a server-side rendering / pre-rendering layer. - The LCP is text and the font is the long pole. Web fonts loaded with
font-display: swapcause a render delay on the H1 LCP. The fix isfont-display: optional, self-hosting the font, and preloading with<link rel="preload" as="font" crossorigin>. The rewriter doesn’t manage your font loading strategy.
Related
- How to fix INP on nginx
- How to fix CLS on nginx
- How to fix LCP on WordPress
- Server-side critical CSS on nginx
- The economics of image optimization
- ModPageSpeed filter reference
ModPageSpeed runs as an nginx or Apache module; a 14-day trial starts from /pagespeed_global_admin (card-at-start via FastSpring). See license terms.
Read next
-
nginx CLS via insert_image_dimensions
How to fix CLS on nginx: rewrite img tags to add width/height, reserve container space, use aspect-ratio, ship dimensions at build time.
-
nginx INP: a CMS-agnostic plan
How to fix INP on nginx: a CMS-agnostic guide. Server-layer JS minification helps if your JS is bloated; if your stack is already lean, the wins are upstream.
-
ASP.NET Core LCP: Razor vs Blazor
How to fix LCP on ASP.NET Core: async view components, cached static files, and the WeAmp.PageSpeed middleware. Practical steps for Razor Pages and MVC in 2026.