Skip to content
Back to Blog
Performance10 min read

Core Web Vitals Optimization 2026: 6 Server Fixes

Push LCP, INP, and CLS into green with six server-side optimizations: compression, cache headers, modern image formats, script loading, and CDN tuning.

Written by Abdul AbrorTechnical Hosting Support Engineer
Core Web Vitals Optimization 2026: 6 Server Fixes
On this page

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.

FAQ

Does hosting type matter for Core Web Vitals?

Yes. Shared hosting with CPU throttling and no HTTP/2 support will cap your scores no matter what you optimize. VPS or managed WordPress hosting gives you room to tune.

Can I get green scores with a page builder?

Elementor, Divi, and WPBakery ship heavy CSS and JS. You can still pass if you defer scripts, optimize images, and use a fast host, but it's harder than a lean theme.

Do I need a CDN to pass Core Web Vitals?

Not strictly, but a CDN cuts latency for distant users and improves LCP. Cloudflare's free tier is enough for most sites.

What if my LCP is still red after compression and caching?

Check your largest element—usually a hero image or background. Preload it with <link rel="preload" as="image" href="hero.jpg"> and make sure it's under 200 KB.

Should I disable all plugins to test?

Disable optimization plugins (caching, minification) temporarily so you measure the server baseline. Once server fixes are live, re-enable them and compare.