A slow website quietly costs you enquiries. Visitors on mobile data stare at a blank screen, tap a button that doesn't respond, or lose their place when the layout jumps, and many of them simply leave. The good news is that most slow sites don't need a rebuild. The usual culprits are oversized images, render-blocking code, too many third-party scripts and weak caching, and all of them can be fixed.
This guide walks through the fixes in the order we usually tackle them, explains which Core Web Vital each one improves, and ends with a checklist you can hand to your developer.
Why speed matters, and what Google actually measures
Speed affects two things you care about: whether visitors stay long enough to convert, and how your pages perform in search. Google uses Core Web Vitals as part of its page experience signals. They won't rescue thin content, but between two similar pages, a poor experience is a disadvantage you don't need.
There are three Core Web Vitals in 2026:
| Metric | What it measures | "Good" threshold |
|---|---|---|
| LCP (Largest Contentful Paint) | How quickly the main content (usually the hero image or headline) appears | 2.5 seconds or less |
| INP (Interaction to Next Paint) | How quickly the page responds visually after a tap, click or keypress | 200 milliseconds or less |
| CLS (Cumulative Layout Shift) | How much the layout jumps around while loading | 0.1 or less |
INP replaced First Input Delay (FID) as a Core Web Vital in March 2024, so if an audit or agency report still quotes FID, it's out of date. Google assesses each metric at the 75th percentile of real visits, so most of your users, not just the ones on fast phones, need a good experience. The full definitions are on web.dev.
Lab scores vs field data
The 0–100 score in PageSpeed Insights is a lab score from Lighthouse: one simulated load on a throttled mid-range phone. It's useful for diagnosing problems, but it isn't what Google uses for ranking. The "Discover what your real users are experiencing" section at the top shows field data from the Chrome UX Report, collected over the previous 28 days. That's the one that counts.
Two practical consequences:
- Don't chase 100/100. A page can score in the 70s in the lab and still pass Core Web Vitals in the field, and the other way round.
- After you ship fixes, field data takes a few weeks to catch up. Judge progress on the lab score and on your own monitoring first.
Step 1: Diagnose before you change anything
Run PageSpeed Insights on your three or four most important templates (homepage, a service or product page, a blog post, a landing page), on mobile. Then open Chrome DevTools' Performance and Network panels on the same pages. You're looking for:
- Page weight and request count. Which files are largest? How many requests come from domains you don't control?
- The LCP element. Lighthouse names it. Is it an image, and how long does it take to arrive?
- Long tasks. Big blocks of JavaScript running on the main thread are the usual cause of poor INP.
- Layout shifts. Lighthouse lists the elements that moved.
For a second opinion, WebPageTest runs tests on real devices from different locations and shows a frame-by-frame filmstrip of how the page loads.
Write the numbers down. Without a baseline you can't tell which change helped.
Step 2: Fix images first
On most small-business sites, images are the single biggest contributor to page weight, and they're the cheapest thing to fix.
- Resize to display size. A 4,000px photo shown in a 1,200px slot wastes bandwidth. Export at roughly twice the displayed width at most.
- Use modern formats. WebP is supported by all current browsers and is usually much smaller than JPEG or PNG at similar quality. AVIF is often smaller still.
- Serve responsive sizes with
srcsetandsizes, so phones download a phone-sized image. - Lazy-load images below the fold with
loading="lazy". - Automate it. Squoosh is fine for a handful of images; for anything more, let your CMS, image CDN or build step (for example with the
sharplibrary) generate the sizes and formats. - Never lazy-load the LCP image. This is a common mistake. Your hero image should load eagerly and get priority instead:
<img src="/images/hero-1200.webp"
srcset="/images/hero-640.webp 640w, /images/hero-1200.webp 1200w"
sizes="100vw"
width="1200" height="600"
fetchpriority="high"
alt="…">
The width and height attributes also let the browser reserve space before the image arrives, which directly reduces CLS.
Photo: Bquast · CC0 · Wikimedia Commons
Step 3: Remove render-blocking CSS and JavaScript
Browsers won't paint anything until they've downloaded and processed the CSS and synchronous scripts in the <head>. That produces the blank-screen effect visitors hate.
- Inline critical CSS. Put the small amount of CSS needed for above-the-fold content directly in the page, and load the rest without blocking rendering. Tools such as critical can extract it during your build. A common pattern for the non-critical stylesheet:
<style>/* critical, above-the-fold CSS inlined here */</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>
- Defer scripts. Add
deferto scripts that don't need to run before the page is shown (most of them). Useasynconly for independent scripts such as some analytics tags, since async scripts run in unpredictable order. - Self-host or preload fonts, and use
font-display: swapso text appears immediately in a fallback font. - Preconnect to critical third-party origins, such as an image CDN, so the connection is ready before the browser discovers the first file. Keep it to two or three origins; preconnecting to everything wastes the benefit.
<link rel="preconnect" href="https://cdn.example.com" crossorigin>
<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin>
Step 4: Ship less code
Every kilobyte of JavaScript has to be downloaded, parsed and executed, and execution is what hurts INP on mid-range Android phones.
- Minify CSS and JavaScript (any modern build tool does this).
- Remove unused code. Lighthouse's "Reduce unused JavaScript/CSS" audits and the DevTools Coverage tab show what's loaded but never used. Old sliders, icon libraries loaded for three icons and whole CSS frameworks used for a grid are common finds.
- Split bundles so each page loads only what it needs, rather than one large file for the whole site. In a React single-page app, route-level splitting takes a few lines:
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router';
const Home = lazy(() => import('./pages/Home'));
const Checkout = lazy(() => import('./pages/Checkout'));
export default function App() {
return (
<BrowserRouter>
<Suspense fallback={<p>Loading…</p>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/checkout" element={<Checkout />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
Frameworks such as Next.js and Astro split code per route automatically. Use a bundle analyser to find large dependencies worth replacing.
- Break up long tasks. If a click triggers heavy work, show visual feedback first and move non-urgent work later, or into a web worker.
Step 5: Get caching right and add a CDN
- Browser caching. Static assets with content-hashed filenames (e.g.
app.3f9a1c.js) can be cached for a year withCache-Control: public, max-age=31536000, immutable. Returning visitors then reuse them without a network request. HTML should use a short cache or revalidation so updates show up. - Compression. Make sure text assets are served with Brotli or gzip.
- CDN. A content delivery network serves files from a location near the visitor. If your server is in the US or Europe and your customers are in India, this alone can noticeably cut latency. Most CDNs also support HTTP/2 and HTTP/3 and can cache full HTML pages for content that rarely changes.
Step 6: Audit third-party scripts
Analytics, ad pixels, chat widgets, heatmaps, social embeds and A/B testing tools all compete with your own code for the same main thread. Each one may also pull in further scripts of its own.
For every third-party script, ask:
- Is anyone using the data? Pixels from past campaigns and abandoned tools are common. Remove them.
- Does it need to load on every page? A booking widget only belongs on the booking page.
- Can it wait? Chat widgets and heatmaps can usually load after the page is interactive, or only when the user scrolls or clicks.
- Can it be replaced with something lighter? A YouTube embed can be a thumbnail that loads the player on click.
For scripts that can wait, a simple approach is to load them on the visitor's first interaction, with a timeout as a fallback:
<script>
function loadChat() {
if (window.chatLoaded) return;
window.chatLoaded = true;
const s = document.createElement('script');
s.src = 'https://chat.example.com/widget.js';
s.async = true;
document.head.appendChild(s);
}
['pointerdown', 'keydown', 'scroll'].forEach((evt) =>
addEventListener(evt, loadChat, { once: true, passive: true })
);
addEventListener('load', () => setTimeout(loadChat, 8000));
</script>
A tag manager makes adding scripts easy, which is exactly why it needs a regular clean-up.
Step 7: Fix the server and the database
If the server itself is slow, nothing on the front end can fully compensate. The metric to watch is Time to First Byte (TTFB). Google's guidance treats 0.8 seconds or less as good.
- Move off overloaded shared hosting if TTFB is consistently high. A modern VPS or managed platform with SSD/NVMe storage, HTTP/2 or HTTP/3 and current PHP/Node versions is a meaningful upgrade for a modest monthly cost.
- Add server-side caching. Page caching (or static generation) means most visitors get a pre-built page instead of the server rebuilding it every time.
- Clean the database. On CMS sites like WordPress, old post revisions, expired transients and spam comments accumulate. Clear them out, then look at slow queries and add indexes or object caching (e.g. Redis) where needed.
- Watch for N+1 queries in custom applications: code that fetches a list, then runs a separate query for each item. Replacing those with a single query using a join, or a batched lookup, often cuts server time sharply.
Photo: NOIRLab/NSF/AURA/T. Slovinský · CC BY 4.0 · Wikimedia Commons
Mapping fixes to Core Web Vitals
When a specific metric is failing, start here:
| Failing metric | Most likely fixes |
|---|---|
| LCP | Optimise and prioritise the hero image (fetchpriority="high", no lazy-loading), reduce TTFB, remove render-blocking CSS/JS, use a CDN |
| INP | Reduce and split JavaScript, break up long tasks, delay or remove third-party scripts, avoid heavy work in click handlers |
| CLS | Set width/height on images and video, reserve space for ads, embeds and banners, use font-display: swap with a well-matched fallback font, avoid inserting content above what's already visible |
Step 8: Keep it fast
Performance tends to slip. Someone uploads an uncompressed banner, marketing adds a new pixel, a plugin update brings in another library. To stop the slide:
- Check field data monthly in Search Console's Core Web Vitals report, which groups URLs by status and template.
- Set a performance budget (for example, a maximum page weight or JavaScript size per template) and check it in your deployment pipeline with Lighthouse CI. Assertions in
lighthouserc.jsonfail the build when a page goes over:
{
"ci": {
"assert": {
"assertions": {
"largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
"cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
"resource-summary:script:size": ["warn", { "maxNumericValue": 300000 }]
}
}
}
}
- Make image optimisation automatic. Have the CMS or build step resize and convert uploads so it doesn't depend on people remembering.
- Review third-party tags quarterly.
If you run a larger site, real-user monitoring that tracks LCP, INP and CLS per page type will spot regressions long before they appear in Search Console. Google's small web-vitals library does the measuring; you only need an endpoint to collect the results:
import { onLCP, onINP, onCLS } from 'web-vitals';
function send(metric) {
const body = JSON.stringify({
name: metric.name,
value: metric.value,
rating: metric.rating,
page: location.pathname,
});
navigator.sendBeacon('/api/vitals', body);
}
onLCP(send);
onINP(send);
onCLS(send);
We cover this as part of our monitoring and observability work.
Key takeaways
- Measure first: record PageSpeed lab scores and field Core Web Vitals for your key templates
- Resize, compress and convert images to WebP/AVIF, lazy-load below the fold, and prioritise the LCP image
- Inline critical CSS and defer non-essential JavaScript
- Remove unused code and split bundles
- Cache static assets for a long time, enable Brotli/gzip and put a CDN in front
- Remove, delay or replace third-party scripts
- Fix slow hosting, add server-side caching and clean the database
- Monitor monthly and enforce a performance budget so the gains last
Speed is rarely one big fix. It comes from many small, deliberate improvements that add up. If you'd like a second pair of eyes, our technical SEO and web application teams will be happy to help. Book a free consultation for a no-obligation chat, and if it's a fit, we can also help make your website DPDP-compliant.
