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 jumps — CLS (Cumulative Layout Shift): late fonts, ads, images without dimensions
- Whether taps feel immediate — INP (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:
- Record a mobile trace (Chrome DevTools Performance / Lighthouse mobile)
- Note LCP element — image, text block, or video poster?
- List long tasks over ~50ms on the main thread
- Count third-party origins on the critical path
- 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
- Correct dimensions — never force a 4000px photo into a 400px slot
- Modern formats — WebP/AVIF where supported, with sensible fallbacks
- Compression — quality 70–85 is often invisible on photos; test on real content
- Lazy-load below the fold — but not the LCP image
- Priority —
fetchpriority="high"on the true LCP image; preload when it helps - 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
- Pick the real path: home → service → contact (or product → cart)
- Test on throttled mobile in DevTools and a physical phone on 4G
- Watch Search Console Core Web Vitals for field regressions after deploy
- Fix the biggest offender first (often one hero image or one script)
- 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
- Week 1: inventory images + fix LCP candidate; set dimensions everywhere critical
- Week 2: cut or defer third parties; audit fonts
- Week 3: caching headers, OPcache, CDN configuration
- 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 →