Skip to content
Back to Blog
Performance10 min read

Improve Core Web Vitals: 7 Fixes That Work in 2026

Server-side tuning, image optimization, and render-blocking fixes that move the needle on LCP, CLS, and INP—with real Lighthouse scores.

Written by Abdul AbrorTechnical Hosting Support Engineer
Improve Core Web Vitals: 7 Fixes That Work in 2026
On this page

Google ranks sites partly on three metrics called Core Web Vitals: Largest Contentful Paint (LCP), Cumulative Layout Shift (CLS), and Interaction to Next Paint (INP). A slow LCP or jumpy CLS can drop you from page one to page three overnight. I've walked hundreds of clients through this, and the same handful of fixes account for most wins.

Below are seven changes that actually move scores—server tuning, image compression, lazy loading, CDN configuration, CSS delivery, and font loading. Each ties directly to one of the three vitals. You'll see what to check on your server, what to add to your theme or plugin stack, and where to look when a metric still won't budge.

1. Enable Brotli or Gzip at the server level

Text compression cuts transfer size by seventy to eighty percent. That means faster LCP because the HTML, CSS, and JavaScript arrive sooner.

Most Apache servers ship with mod_deflate; newer ones include mod_brotli. Brotli compresses tighter than gzip—often five to fifteen percent smaller—but both beat sending uncompressed bytes.

Add this block to your .htaccess if you're on shared cPanel hosting:

<IfModule mod_deflate.c>
  AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/javascript application/json application/xml
</IfModule>

For nginx, open /etc/nginx/nginx.conf and confirm these lines exist inside the http block:

gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
gzip_vary on;

Reload nginx with sudo systemctl reload nginx. Check compression by running:

curl -H "Accept-Encoding: gzip" -I https://yoursite.com

Look for Content-Encoding: gzip in the headers. No header means compression isn't active.

Brotli requires a newer build. On Ubuntu 22.04 or later, install libnginx-mod-http-brotli-filter and add:

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

Restart nginx and verify with Accept-Encoding: br in your curl command. Most CDNs auto-compress at the edge, but server-level compression helps when the origin response is slow or the CDN cache is cold.

2. Convert images to WebP and serve responsive sizes

Oversized JPEGs kill LCP. A single 2 MB hero image can add two seconds to your paint time on a mobile connection.

WebP delivers the same visual quality at forty to sixty percent smaller file sizes than JPEG. AVIF is even tighter but browser support lags; stick with WebP for now.

If you're on WordPress, install an image optimization plugin that converts on upload. I've had good results with ShortPixel and Imagify. Both handle bulk conversion of your existing library and serve WebP with a <picture> fallback.

For manual conversion on the server:

sudo apt install webp
cwebp -q 85 hero.jpg -o hero.webp

Then update your HTML to serve multiple sizes:

<picture>
  <source srcset="hero-480.webp 480w, hero-960.webp 960w, hero-1920.webp 1920w" type="image/webp">
  <img src="hero.jpg" alt="Hero image" loading="lazy" width="1920" height="1080">
</picture>

The browser picks the smallest file that fits the viewport. Explicit width and height attributes prevent layout shift because the browser reserves space before the image loads—critical for CLS.

3. Lazy-load images and iframes below the fold

Loading twenty images at page load wastes bandwidth and delays LCP. Native lazy loading defers offscreen images until the user scrolls near them.

Add loading="lazy" to every <img> and <iframe> that sits below the first screen:

<img src="screenshot.webp" alt="Dashboard" loading="lazy" width="800" height="600">
<iframe src="https://www.youtube.com/embed/VIDEO_ID" loading="lazy"></iframe>

Do not lazy-load your hero image or anything visible on page load. That will delay LCP instead of helping it.

WordPress adds loading="lazy" automatically since version 5.5, but many themes and page builders strip it or apply it incorrectly. Inspect your largest image in DevTools; if the attribute is missing, your theme is overriding core behavior. Switch the image block to a standard WordPress image or edit your theme's functions.php:

add_filter('wp_lazy_loading_enabled', '__return_true');

For non-WordPress sites, a single attribute is all you need. Browser support is near-universal; legacy browsers simply load the image immediately.

4. Eliminate render-blocking CSS in the critical path

Every external stylesheet in your <head> blocks rendering until the CSS downloads and parses. The browser won't paint a single pixel until it has all the styles.

Inline critical CSS—the rules needed for above-the-fold content—directly into a <style> tag in the <head>. Load the rest asynchronously:

<style>
  /* Critical CSS: header, hero, layout */
  body { margin: 0; font-family: sans-serif; }
  .header { background: #333; color: #fff; padding: 1rem; }
</style>
<link rel="preload" href="/css/main.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/css/main.css"></noscript>

The preload trick loads the stylesheet in the background and applies it once ready. The <noscript> fallback covers users with JavaScript disabled.

Extracting critical CSS by hand is tedious. Tools like Critical or Critters automate it, but they require a build step. For WordPress, plugins like WP Rocket and Autoptimize offer one-click critical CSS generation. In my experience, automatic extraction works well on simple themes; complex page builders produce bloated inline blocks that hurt more than they help.

Check your render-blocking resources in Lighthouse under "Opportunities." Anything over 50 KB probably doesn't belong in the critical path.

5. Preconnect to third-party domains and prefetch DNS

Every external font, analytics script, or ad network adds a DNS lookup, TCP handshake, and TLS negotiation. Those round trips add up—often 200 to 400 milliseconds on mobile.

Tell the browser to connect early with rel="preconnect":

<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preconnect" href="https://www.google-analytics.com">

Use rel="dns-prefetch" for domains you might need but aren't certain:

<link rel="dns-prefetch" href="https://cdn.example.com">

preconnect does DNS + TCP + TLS; dns-prefetch does DNS only. Limit preconnect to three or four critical domains because each connection costs memory.

Google Fonts is a common bottleneck. Switching to self-hosted fonts eliminates the external connection entirely. Download the font files, add them to /fonts, and declare them in your CSS:

@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-var.woff2') format('woff2');
  font-display: swap;
}

font-display: swap shows fallback text immediately and swaps in the web font once loaded. That prevents invisible text (FOIT) and helps INP because the page is interactive sooner.

6. Configure your CDN to cache HTML and tune cache headers

Most CDNs cache images and static assets by default but skip HTML. That's fine for logged-in users, but anonymous visitors should get cached HTML to speed up Time to First Byte (TTFB), which feeds into LCP.

Cloudflare offers Page Rules to cache everything on specific URLs:

  1. Go to Caching > Page Rules in your Cloudflare dashboard.
  2. Add a rule: yoursite.com/*
  3. Set Cache Level to "Cache Everything."
  4. Set Edge Cache TTL to 2 hours or 4 hours.
  5. Save and deploy.

Purge the cache whenever you publish new content. Most WordPress caching plugins (WP Rocket, W3 Total Cache) include a Cloudflare integration that purges automatically.

For Apache, set proper Cache-Control headers so the CDN knows what to cache:

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

Nginx equivalent:

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

Immutable assets (fonts, images with versioned filenames) can have year-long cache times. CSS and JS should be shorter if you don't use cache-busting query strings.

7. Defer or async non-critical JavaScript

JavaScript is the leading cause of poor INP. Every script that runs on page load competes with user input. A heavy analytics bundle or chat widget can delay button clicks by hundreds of milliseconds.

Add defer to scripts that don't need to run immediately:

<script src="/js/analytics.js" defer></script>
<script src="/js/social-share.js" defer></script>

defer downloads the script in parallel but waits until HTML parsing finishes before executing. Use async for truly independent scripts like ad tags:

<script src="https://ads.example.com/tag.js" async></script>

async runs the script as soon as it downloads, which can still block the main thread. Reserve it for third parties you can't control.

WordPress themes often enqueue jQuery and dozens of plugins in the <head> without defer. Move them to the footer or add defer with a filter:

function add_defer_attribute($tag, $handle) {
  if (is_admin()) return $tag;
  return str_replace(' src', ' defer src', $tag);
}
add_filter('script_loader_tag', 'add_defer_attribute', 10, 2);

Test after adding this filter—some plugins expect jQuery to be available immediately and will break. In that case, exclude those handles from the filter.

What if Lighthouse scores still don't improve?

Run Lighthouse three times and average the scores; single runs vary by ±10 points depending on CPU throttling and network jitter. Test on a real mobile device when possible—emulated mobile in DevTools is less accurate.

Check field data in Google Search Console under Core Web Vitals. Lab data (Lighthouse) measures synthetic performance; field data measures real users. A site can score 95 in Lighthouse but fail in the field if most visitors are on slow connections or old phones.

Look at your server response time in the Lighthouse report. If TTFB is over 600 milliseconds, your bottleneck is backend—database queries, PHP execution, or hosting resource limits. Upgrade your hosting plan, enable object caching (Redis or Memcached), or switch to a faster stack.

Why does my LCP change between tests?

LCP measures the largest visible element, and that element can change depending on viewport size or what loads first. On mobile the hero image might be largest; on desktop it could be a text block. Run separate mobile and desktop audits to see which element Lighthouse identifies each time.

Does lazy loading hurt SEO?

No. Google renders JavaScript and respects loading="lazy". The attribute is a hint, not a block. Images below the fold load when Googlebot scrolls the page during indexing.

Can I fix CLS without changing my layout?

Yes. Reserve space with width and height attributes on every image, iframe, and video. Set explicit dimensions on ad slots even if the ad size varies—use min-height on the container so the page doesn't jump when the ad loads.

Should I self-host Google Fonts or use the CDN?

Self-hosting eliminates the external DNS lookup and gives you full cache control, but you lose automatic font updates and subset optimizations. For most sites, self-hosting wins—especially if you're already using a CDN that caches your static assets globally.

How often should I test Core Web Vitals?

Run Lighthouse after every deploy. Set up a CI check with Lighthouse CI to catch regressions before they hit production. Monitor Search Console weekly to catch field data changes early.

What to check first

Start with compression and image optimization. Those two fixes account for the biggest LCP improvements in my support queue. Add lazy loading next, then tackle render-blocking CSS and JavaScript.

Don't optimize for a perfect 100 score—focus on getting out of the "Poor" range in Search Console field data. A score of 85 with fast field metrics beats a lab score of 98 that doesn't match real-world usage.

Measure before and after. Note your baseline LCP, CLS, and INP in Lighthouse, apply one fix, and test again. That way you know which changes actually helped.