Skip to content
Back to Blog
Performance9 min read

Core Web Vitals Optimization: 7 Server-Side Fixes

Improve LCP, FID, and CLS scores with server configuration changes, caching rules, and compression tweaks—no frontend refactoring required.

Written by Abdul AbrorTechnical Hosting Support Engineer
Core Web Vitals Optimization: 7 Server-Side Fixes
On this page

Most Core Web Vitals guides focus on minifying JavaScript or lazy-loading images. That's fine, but half the battle happens before a single byte of HTML reaches the browser. Server configuration, caching layers, and transport compression can cut Largest Contentful Paint by seconds and eliminate layout shifts caused by slow resource delivery.

I've watched sites jump from red to green Core Web Vitals scores with zero frontend changes. The fixes below target LCP, FID, and CLS from the server side—Apache, NGINX, caching engines, and CDN rules you control from cPanel or SSH.

Enable HTTP/2 or HTTP/3

HTTP/1.1 opens one TCP connection per resource. Six CSS files means six round trips. HTTP/2 multiplexes all requests over a single connection and prioritizes critical resources, cutting LCP by eliminating connection overhead.

Check your current protocol:

curl -I --http2 https://yourdomain.com | grep -i http

If you see HTTP/1.1, enable HTTP/2 in Apache:

LoadModule http2_module modules/mod_http2.so
Protocols h2 h2c http/1.1

For NGINX:

listen 443 ssl http2;
listen [::]:443 ssl http2;

Restart the web server. HTTP/2 requires SSL, so make sure your certificate is active. Most cPanel servers running EasyApache 4 support HTTP/2 out of the box; just flip it on in MultiPHP Manager if the option appears.

HTTP/3 (QUIC) is newer and cuts another round trip by using UDP instead of TCP. Cloudflare enables it automatically for proxied domains. On your origin server, NGINX 1.25+ ships with experimental QUIC support:

listen 443 quic reuseport;
add_header Alt-Svc 'h3=":443"; ma=86400';

Not every hosting panel exposes HTTP/3 yet. If yours doesn't, HTTP/2 is still a huge win.

Compress responses with Brotli

Gzip has been the standard for years. Brotli compresses text assets 15-20% smaller than gzip at comparable speed, which means faster LCP for HTML, CSS, and JavaScript.

Apache with mod_brotli:

<IfModule mod_brotli.c>
  AddOutputFilterByType BROTLI_COMPRESS text/html text/plain text/xml text/css text/javascript application/javascript application/json
  BrotliCompressionQuality 6
</IfModule>

NGINX with ngx_brotli:

brotli on;
brotli_types text/plain text/css application/json application/javascript text/xml application/xml;
brotli_comp_level 6;

You'll need to compile the Brotli module if your distribution doesn't include it. On Ubuntu:

sudo apt install libbrotli-dev

Then rebuild NGINX with --add-module=../ngx_brotli. Shared hosting users can ask their provider to enable Brotli, or set up Cloudflare in front of the site—Cloudflare compresses with Brotli automatically.

Keep gzip enabled as a fallback. Older browsers that don't speak Brotli will still get compressed responses.

Use a full-page cache

Dynamic page generation is slow. WordPress or Laravel hits the database, renders a template, and builds HTML on every request. That delay becomes your Time to First Byte and pushes LCP higher.

Full-page caching stores the final HTML and serves it instantly. For WordPress, install LiteSpeed Cache, WP Rocket, or W3 Total Cache. I prefer LiteSpeed Cache on LiteSpeed servers and Redis Object Cache + Breeze on Apache.

Set a reasonable TTL—3600 seconds for most pages, shorter for frequently updated content:

<IfModule mod_expires.c>
  ExpiresActive On
  ExpiresByType text/html "access plus 1 hour"
</IfModule>

For NGINX with FastCGI caching:

fastcgi_cache_path /var/cache/nginx levels=1:2 keys_zone=MYAPP:100m inactive=60m;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_cache_valid 200 60m;

Bypass the cache for logged-in users and POST requests. Most caching plugins handle this automatically. Verify by checking response headers:

curl -I https://yourdomain.com | grep -i cache

You should see X-Cache: HIT or X-LiteSpeed-Cache: hit.

Push critical resources early

HTTP/2 Server Push sends CSS and JavaScript to the browser before it even asks, eliminating a round trip. The browser starts parsing styles immediately, reducing render-blocking time and improving LCP.

Apache with mod_http2:

<Location />
  H2PushResource add /css/critical.css
  H2PushResource add /js/main.js
</Location>

NGINX:

location / {
  http2_push /css/critical.css;
  http2_push /js/main.js;
}

Push only critical resources—your above-the-fold CSS and primary script bundle. Pushing too much wastes bandwidth and can slow things down. In support tickets I handled, the usual mistake was pushing every asset on the page.

Alternatively, use Link preload headers:

Header add Link "</css/critical.css>; rel=preload; as=style"

Browsers will fetch these resources in parallel with the HTML parse. Preload is safer than Server Push because the browser can decide whether it already has the resource cached.

Tune your database connection pool

Slow database queries inflate TTFB and delay LCP. If your server runs out of MySQL connections, requests queue up and FID suffers.

Check current connections:

SHOW STATUS LIKE 'Threads_connected';
SHOW VARIABLES LIKE 'max_connections';

If Threads_connected hovers near max_connections, raise the limit:

[mysqld]
max_connections = 200

Restart MySQL. On a VPS with 4 GB RAM, 150-200 connections is reasonable. More connections consume more memory, so balance against available RAM.

Enable the query cache if your MySQL version supports it:

query_cache_type = 1
query_cache_size = 64M

MySQL 8.0 removed the query cache. Use Redis or Memcached instead.

For WordPress, install Redis Object Cache:

sudo apt install redis-server php-redis
sudo systemctl enable redis-server

Add the plugin, enable object caching, and watch your database load drop. Cached queries return in microseconds instead of milliseconds, cutting TTFB.

Set aggressive browser caching headers

Static assets—images, fonts, CSS, JS—should be cached for months. Every cache miss adds a request that delays LCP.

Apache:

<IfModule mod_expires.c>
  ExpiresActive On
  ExpiresByType image/jpeg "access plus 1 year"
  ExpiresByType image/png "access plus 1 year"
  ExpiresByType image/webp "access plus 1 year"
  ExpiresByType text/css "access plus 1 year"
  ExpiresByType application/javascript "access plus 1 year"
  ExpiresByType font/woff2 "access plus 1 year"
</IfModule>

NGINX:

location ~* \.(jpg|jpeg|png|gif|webp|css|js|woff2)$ {
  expires 1y;
  add_header Cache-Control "public, immutable";
}

The immutable directive tells browsers the file will never change, so don't revalidate it even on refresh. Pair this with cache-busting query strings or hashed filenames (styles.a3f9b2c.css) so you can push updates when needed.

For HTML, set a shorter TTL:

location / {
  expires 1h;
  add_header Cache-Control "public, must-revalidate";
}

Verify headers:

curl -I https://yourdomain.com/css/style.css | grep -i cache-control

You want Cache-Control: public, max-age=31536000, immutable.

Optimize image delivery at the server level

Large images kill LCP. Yes, you can resize them in Photoshop, but the server can handle this automatically.

Install mod_pagespeed (Apache) or ngx_pagespeed (NGINX). PageSpeed rewrites HTML on the fly, converting images to WebP, lazy-loading below-the-fold content, and inlining critical CSS:

wget https://dl.google.com/dl/page-speed/psol/1.13.35.2-x64.tar.gz
tar -xzvf 1.13.35.2-x64.tar.gz

Configure:

ModPagespeed on
ModPagespeedEnableFilters rewrite_images,convert_jpeg_to_webp,lazyload_images

For NGINX:

pagespeed on;
pagespeed EnableFilters rewrite_images,convert_jpeg_to_webp,lazyload_images;

PageSpeed can be heavy on CPU. Test with a staging environment first. If it's too much overhead, offload image optimization to a CDN like Cloudflare or BunnyCDN—both convert to WebP and resize on the edge.

Alternatively, use NGINX image_filter module for on-the-fly resizing:

location ~* \.(jpg|jpeg|png)$ {
  image_filter resize 800 600;
  image_filter_buffer 10M;
}

This resizes every image to 800x600 at request time. Not ideal for production without caching, but useful for quick fixes.

What order should you apply these fixes?

Start with HTTP/2 and Brotli—they're low-risk and give instant results. Then set up full-page caching; that's your biggest LCP win. After that, tune database connections and browser cache headers. Server Push and PageSpeed are advanced; try them once the basics are solid.

Run Lighthouse or PageSpeed Insights after each change. You should see LCP drop and FID improve. CLS usually stays stable unless you had render-blocking resources that are now delivered faster, causing late layout shifts—but better caching and compression typically reduce those.

How do I know if HTTP/2 is actually helping?

Compare waterfall charts in Chrome DevTools Network tab before and after. With HTTP/2, resources load in parallel rather than queued. You'll see a denser waterfall with overlapping bars instead of a staircase pattern.

Does full-page caching break personalized content?

Yes, if you cache HTML with user-specific data. Exclude logged-in users from the cache, or use ESI (Edge Side Includes) to inject personalized fragments. Most caching plugins handle this automatically.

Can I use Brotli and gzip at the same time?

Yes. Enable both. The server picks Brotli if the client supports it, otherwise falls back to gzip. No conflict.

Will Server Push slow down repeat visitors?

It can, if you push resources the browser already cached. Use the nopush condition to skip pushes when the client sends a cookie or specific header indicating a cached copy. Or just use preload headers instead—they're safer.

Check your cache headers first

Before you rewrite your entire stack, verify what your server is already sending. Run curl -I on a few URLs. If you see Cache-Control: no-cache on static assets or missing Expires headers, you've found low-hanging fruit. Fix those first, enable HTTP/2, add Brotli, and watch your Web Vitals scores climb. The frontend can wait.