Google judges your site on three metrics: Largest Contentful Paint (LCP), Interaction to Next Paint (INP), and Cumulative Layout Shift (CLS). Get all three into green and you pass Core Web Vitals. Miss even one and you lose ranking juice.
Most guides dump a laundry list of fixes without context. This one walks you through six server and CDN changes that move the needle. I'll show you what to enable, where to set it, and how to verify the change stuck. You don't need a new theme or expensive plugins—just control over your hosting stack.
Why server-side fixes matter more than theme tweaks
Themes and page builders get blamed for slow scores, but the server delivers every byte. A misconfigured cache header forces the browser to re-fetch assets on every visit. Missing compression doubles transfer time. Wrong image formats waste bandwidth.
In support tickets I've handled, the pattern is always the same: the site owner installs five optimization plugins, sees no improvement, then discovers the server wasn't compressing anything. Plugins can't fix what the server never sent.
Start at the origin. Fix compression, caching, and image delivery first. Then layer on client-side tricks if needed.
Fix 1: Enable Brotli and Gzip compression
Uncompressed HTML, CSS, and JavaScript files kill LCP. A typical WordPress homepage is around 200 KB of HTML before compression. Gzip brings that down to 50 KB. Brotli squeezes it further to 45 KB.
Most Apache and Nginx installs ship with Gzip enabled, but Brotli support is patchy. Check what your server actually sends:
curl -I -H "Accept-Encoding: br,gzip" https://yoursite.com
Look for Content-Encoding: br or Content-Encoding: gzip in the response headers. If you see neither, compression is off.
Apache: mod_deflate and mod_brotli
For Gzip, enable mod_deflate and add this to your .htaccess or virtual host config:
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/javascript application/json application/xml
</IfModule>
Brotli requires the mod_brotli module. On cPanel servers running EasyApache 4, install it through WHM or ask your host. Then add:
<IfModule mod_brotli.c>
AddOutputFilterByType BROTLI_COMPRESS text/html text/plain text/xml text/css text/javascript application/javascript application/json
</IfModule>
Nginx: gzip and brotli modules
Gzip is usually on by default. Verify in /etc/nginx/nginx.conf:
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
gzip_min_length 1000;
For Brotli, compile the ngx_brotli module or install it from your distro's package manager. Then add to the http block:
brotli on;
brotli_types text/plain text/css application/json application/javascript text/xml application/xml;
Reload Nginx and test again with curl.
Fix 2: Set long-lived cache headers for static assets
Browsers won't cache assets unless you tell them to. By default, many servers send no Cache-Control header or set it to a few hours. That means repeat visitors re-download CSS, JS, and images every session.
Set a one-year max-age for static files. Append a version query string or hash to the filename when you update them.
Apache .htaccess
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/jpg "access plus 1 year"
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/webp "access plus 1 year"
ExpiresByType image/avif "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 block
location ~* \.(jpg|jpeg|png|webp|avif|css|js|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
The immutable directive tells the browser the file will never change at that URL, so skip revalidation requests.
Fix 3: Serve images in WebP and AVIF formats
JPEG and PNG are outdated. WebP cuts file size by 30% with no visible quality loss. AVIF goes further, especially for photos, but browser support is still catching up.
Serve AVIF with a WebP fallback and a JPEG/PNG backup. Modern browsers pick the best format automatically.
Convert images with cwebp and avifenc
Install the tools:
# Debian/Ubuntu
sudo apt install webp libavif-bin
# CentOS/RHEL
sudo yum install libwebp-tools libavif-tools
Convert a single image:
cwebp -q 80 input.jpg -o output.webp
avifenc --min 20 --max 30 input.jpg output.avif
For bulk conversion, loop through your upload directory:
find /var/www/html/wp-content/uploads -name '*.jpg' -exec sh -c 'cwebp -q 80 "$1" -o "${1%.jpg}.webp"' _ {} \;
Serve with picture element or content negotiation
If you control the HTML, use the <picture> tag:
<picture>
<source srcset="image.avif" type="image/avif">
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="Description">
</picture>
Or let the server pick based on the Accept header. In Nginx:
location ~* \.(jpg|jpeg|png)$ {
set $img_avif "";
set $img_webp "";
if ($http_accept ~* "image/avif") {
set $img_avif "A";
}
if (-f "${request_filename}.avif") {
set $img_avif "${img_avif}F";
}
if ($img_avif = "AF") {
rewrite ^(.*)$ $1.avif break;
}
if ($http_accept ~* "image/webp") {
set $img_webp "W";
}
if (-f "${request_filename}.webp") {
set $img_webp "${img_webp}F";
}
if ($img_webp = "WF") {
rewrite ^(.*)$ $1.webp break;
}
}
Apache needs mod_rewrite and similar logic. Or use a WordPress plugin that generates WebP/AVIF on upload and rewrites image tags.
Fix 4: Defer and async JavaScript loading
Blocking scripts delay LCP and inflate INP. Every <script src="..."> without defer or async stops the browser from rendering until the script downloads and runs.
Defer scripts that don't need to run before the page paints. Async scripts that are truly independent of DOM state.
Defer by default
Add defer to scripts in your theme's functions.php:
function defer_scripts($tag, $handle, $src) {
if (is_admin()) {
return $tag;
}
return str_replace('<script ', '<script defer ', $tag);
}
add_filter('script_loader_tag', 'defer_scripts', 10, 3);
That defers everything. Whitelist inline scripts or third-party analytics if they break.
Load third-party scripts after interaction
Google Tag Manager, Facebook Pixel, and chat widgets can wait. Load them after the user scrolls or clicks:
let loaded = false;
function loadThirdParty() {
if (loaded) return;
loaded = true;
const script = document.createElement('script');
script.src = 'https://example.com/analytics.js';
document.body.appendChild(script);
}
window.addEventListener('scroll', loadThirdParty, { once: true });
window.addEventListener('click', loadThirdParty, { once: true });
This keeps them out of the initial render path and improves both LCP and INP.
Fix 5: Eliminate render-blocking CSS
CSS blocks rendering by default. The browser won't paint until it downloads and parses every stylesheet in the <head>.
Inline critical CSS—the styles needed for above-the-fold content—and defer the rest.
Extract critical CSS
Use a tool like Critical or Penthouse to generate inline styles. On a dev copy of your site:
npm install -g critical
critical https://yoursite.com --inline > critical.css
Paste the output into a <style> tag in your header. Then load the full stylesheet with media="print" and switch it on load:
<link rel="stylesheet" href="/style.css" media="print" onload="this.media='all'">
<noscript><link rel="stylesheet" href="/style.css"></noscript>
The browser fetches the full CSS without blocking render.
Use font-display: swap for web fonts
Web fonts block text rendering until they load. Set font-display: swap to show system fonts immediately:
@font-face {
font-family: 'YourFont';
src: url('/fonts/yourfont.woff2') format('woff2');
font-display: swap;
}
Text appears instantly. The custom font swaps in when ready.
Fix 6: Tune Cloudflare caching and optimization rules
If you're behind Cloudflare, the CDN can override your origin headers and inject optimizations. Check what's actually enabled.
Cache level and edge cache TTL
Go to Caching → Configuration. Set Cache Level to Standard (caches static files) or Cache Everything with a Page Rule for HTML.
For static assets, set a Page Rule with Edge Cache TTL of one month:
- URL:
yoursite.com/wp-content/uploads/* - Cache Level: Cache Everything
- Edge Cache TTL: 1 month
Auto Minify and Rocket Loader
Speed → Optimization:
- Auto Minify: enable CSS and JS. Leave HTML off if it breaks inline scripts.
- Rocket Loader: defer JS automatically, but test thoroughly—it rewrites script tags and can break jQuery plugins.
Early Hints
Enable Early Hints to send Link headers (preload, preconnect) before the origin responds. Shaves 100-200ms off LCP on slow origins.
Argo and Polish
Argo routes traffic over Cloudflare's private backbone (paid). Polish converts images to WebP on the fly (paid, or included with Pro+).
If you already converted images to WebP at the origin, you don't need Polish. But if you can't touch the origin, Polish is the easiest path.
Measure before and after with PageSpeed Insights
Don't guess. Run a baseline test before you start and compare after each fix.
Visit PageSpeed Insights, paste your URL, and note the scores for LCP, INP, and CLS under "Field Data" (real users) and "Lab Data" (simulated).
Field data reflects actual traffic over the past 28 days. Lab data shows the immediate effect of changes but runs under throttled 4G conditions. Treat lab scores as directional; field data is the real verdict.
After each fix, clear Cloudflare cache (if applicable), purge your site cache, and re-test. Wait a few days for field data to update.
What about INP vs FID?
Interaction to Next Paint replaced First Input Delay in 2024. INP measures all interactions, not just the first tap. It's stricter. Deferred scripts and reduced main-thread work are the main levers.
What to check first
Run a quick audit before touching anything. Use your browser's DevTools Network tab: reload your homepage and sort by size. If HTML is over 100 KB uncompressed, compression is off or broken. If images are JPEG and over 500 KB each, format conversion will help. If you see a dozen scripts loading in the head without defer, that's your INP problem.
Fix the biggest bottleneck first, measure, then move to the next one. Six fixes sounds like a lot, but each takes under an hour if you have server access. Green scores are possible. You just need to start at the right layer.
