Website performance clients can actually feel

Core Web Vitals in plain English: what moves LCP, CLS and responsiveness on real UK business sites — images, fonts, caching, third parties and hosting.

Clients don’t open Lighthouse on a Monday morning. They feel whether the hero appears, whether the page jumps while loading, and whether buttons respond. Performance work that only chases a vanity 100 score without fixing those moments is theatre.

Here’s a technical but practical map of what actually moves the needle on real business websites — especially UK service and ecommerce sites where mobile traffic and paid ads make slow pages expensive.

The three feelings that map to metrics

  • How fast something useful shows — largely LCP (Largest Contentful Paint): often the hero image or main headline block
  • Whether the layout jumpsCLS (Cumulative Layout Shift): late fonts, ads, images without dimensions
  • Whether taps feel immediateINP (Interaction to Next Paint): heavy main-thread JS, long tasks

Field data (CrUX / Search Console) beats lab-only scores. Lab is for diagnosis; field is for truth. A site can score well in a clean lab run and still feel sluggish once chat widgets, cookie banners and marketing tags load in the real world.

A slow “beautiful” site loses to a fast credible one more often than agencies admit.

Find the real bottleneck first

Before rewriting CSS or swapping hosts, identify the dominant problem:

  1. Record a mobile trace (Chrome DevTools Performance / Lighthouse mobile)
  2. Note LCP element — image, text block, or video poster?
  3. List long tasks over ~50ms on the main thread
  4. Count third-party origins on the critical path
  5. Check Time to First Byte (TTFB) — if the server is slow, front-end polish only helps so much

Fix in order of user impact. One oversized hero often beats a week of micro-optimisations.

Images: usually the biggest win

  1. Correct dimensions — never force a 4000px photo into a 400px slot
  2. Modern formats — WebP/AVIF where supported, with sensible fallbacks
  3. Compression — quality 70–85 is often invisible on photos; test on real content
  4. Lazy-load below the fold — but not the LCP image
  5. Priorityfetchpriority="high" on the true LCP image; preload when it helps
  6. Responsive srcset — serve smaller files to phones
<img
  src="/assets/hero-1200.webp"
  srcset="/assets/hero-640.webp 640w, /assets/hero-1200.webp 1200w"
  sizes="(max-width: 700px) 100vw, 1200px"
  width="1200" height="675"
  fetchpriority="high"
  alt="Team workshop in a bright studio"
>

Always set width and height (or aspect-ratio CSS) to protect CLS. For background images used as heroes, consider a real <img> with object-fit instead — browsers optimise LCP candidates more predictably that way.

For product grids and blogs:

  • Generate thumbnails at display size, not “full size scaled down in CSS”
  • Use consistent aspect ratios so cards don’t reflow as images arrive
  • Avoid loading a carousel’s full gallery before the first slide is visible

Fonts without layout thrash

  • Limit families and weights — each file costs
  • Use font-display: swap (or optional) deliberately
  • Self-host when you can; subset if the alphabet allows
  • Avoid invisible text for long periods (FOIT) that feels broken
  • Preload only the critical weight used above the fold
@font-face {
  font-family: "Display";
  src: url("/fonts/display.woff2") format("woff2");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}

System font stacks are a legitimate performance choice for tools and admin UIs. Marketing brands often want a display face — then keep body text simpler so you are not downloading six weights of two families on every page.

CSS and JavaScript weight

  • Don’t load an entire UI framework for a five-page brochure site
  • Critical CSS can help; unused CSS rarely gets purged automatically without tooling
  • Defer non-critical JS; split admin-only code from the public homepage
  • Long tasks (>50ms) hurt INP — break work up or move it off the critical path
  • Prefer CSS for simple open/close states when possible; reserve JS for real behaviour
<!-- Non-critical script: parse HTML first -->
<script src="/assets/js/enhancements.js" defer></script>

If you use a page builder, measure the DOM size. Thousands of nested wrappers make style calculation and interaction slower even when “the image is optimised”.

Third parties: the silent budget killers

Chat widgets, tag managers, heatmaps, embeds and “just one more pixel” add up. Treat each as a negotiation:

  • Does it load on every page or only where needed?
  • Can it wait until interaction (cookie consent, open chat)?
  • Is there a lighter alternative or first-party version?
  • Does marketing still use the data, or is the tag legacy?

A practical pattern: load analytics after consent and idle time; load chat on click of “Chat with us” rather than on every first paint. You keep the capability without paying the cost on every visit.

Caching, CDN and hosting

  • Static assets with long cache headers + fingerprinting (?v= or hashed filenames)
  • HTML with short cache or revalidation so updates show up
  • PHP OPcache on — common miss on cheap hosts
  • Object cache for heavy WordPress/DB pages when traffic justifies it
  • Geography — UK audience? Prefer UK/EU edge, not only a distant origin
  • HTTP/2 or HTTP/3 with modern TLS — confirm at the host, not only in theory
# Example idea for fingerprinted assets (concept)
Cache-Control: public, max-age=31536000, immutable

A cheap host with noisy neighbours is a false economy if the site generates leads. Watch TTFB under load, not only on a single quiet request from your office fibre.

WordPress-specific notes

  • Page builders can emit huge DOM and CSS — measure before blaming “WordPress”
  • Image plugins help only if configured; originals still need discipline
  • Limit plugins that inject front-end scripts globally
  • Transients and object cache matter more as traffic grows
  • Turn off emoji and other default scripts you do not need if they add noise
  • Full-page cache for anonymous visitors can transform category and blog pages

WooCommerce category and product templates deserve special attention: uncached queries, large image galleries and review widgets often dominate. Cache smartly, and keep product images in modern formats at sensible sizes.

Core Web Vitals in day-to-day language

Use these mental models with stakeholders:

  • LCP — “How long until the main thing on the page looks ready?”
  • CLS — “Does anything jump while I’m trying to tap?”
  • INP — “When I tap, does the page react quickly?”

Improvements that clients notice without a report: hero appears sooner, sticky header doesn’t shove content, buttons respond on first tap, cookie banner doesn’t cause a layout leap.

How to measure like a human

  1. Pick the real path: home → service → contact (or product → cart)
  2. Test on throttled mobile in DevTools and a physical phone on 4G
  3. Watch Search Console Core Web Vitals for field regressions after deploy
  4. Fix the biggest offender first (often one hero image or one script)
  5. Re-measure the same path — don’t celebrate a homepage win if checkout got worse

Create a short performance checklist in the project README so the next change doesn’t silently reintroduce a 2MB hero or a global tag.

A 30-day improvement plan

  1. Week 1: inventory images + fix LCP candidate; set dimensions everywhere critical
  2. Week 2: cut or defer third parties; audit fonts
  3. Week 3: caching headers, OPcache, CDN configuration
  4. Week 4: JS budget on key templates; re-test field metrics after traffic settles

You rarely need a total rebuild. You need disciplined defaults and a habit of measuring the journeys that make money.

Worked examples (performance)

Concrete snippets you can drop into templates and hosting configs.

1) Responsive hero image (LCP-friendly)

<link rel="preload" as="image"
  href="/assets/hero-1200.webp"
  imagesrcset="/assets/hero-640.webp 640w, /assets/hero-1200.webp 1200w"
  imagesizes="100vw">

<img
  src="/assets/hero-1200.webp"
  srcset="/assets/hero-640.webp 640w, /assets/hero-1200.webp 1200w"
  sizes="100vw"
  width="1200"
  height="675"
  alt=""
  fetchpriority="high"
  decoding="async"
>

2) Font loading without layout thrash

@font-face {
  font-family: "Display";
  src: url("/fonts/display.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

/* Reserve space so text doesn’t jump when the face swaps in */
.hero-title {
  font-family: "Display", system-ui, sans-serif;
  line-height: 1.08;
}

3) Defer non-critical JS

<!-- Critical path: HTML first -->
<script src="/assets/js/main.js" defer></script>

<!-- Chat / analytics: load after idle if you must -->
<script>
requestIdleCallback?.(() => {
  const s = document.createElement('script');
  s.src = 'https://example.com/chat.js';
  s.async = true;
  document.body.appendChild(s);
});
</script>

4) Cache headers (concept for static assets)

# Fingerprinted assets can cache hard
# /assets/app.a1b2c3.js  →  Cache-Control: public, max-age=31536000, immutable

# HTML stays short-lived so launches show up
# /index.php             →  Cache-Control: public, max-age=60, must-revalidate

5) Quick lab check (DevTools)

// In the console after a hard reload on mobile throttle:
performance.getEntriesByType('paint');
// Look for first-contentful-paint timing
// Then Network panel → disable cache → slow 4G → watch LCP element

Want a performance pass on an existing site? Send the URL →