Core Web Vitals Optimisation Guide
Last updated
Core Web Vitals are Google’s three page experience metrics: Largest Contentful Paint (LCP), Interaction to Next Paint (INP), and Cumulative Layout Shift (CLS). This guide covers how to diagnose and fix problems with each. For definitions and thresholds, see Core Web Vitals.
Tools
Before fixing anything, confirm where the problem actually is.
Field data (real user data):
- Google Search Console — Core Web Vitals report shows field data by URL group, segmented by mobile and desktop. This is the authoritative source for whether your pages pass or fail.
- PageSpeed Insights — shows both field data (from the Chrome User Experience Report) and lab data for a specific URL. Field data here matches what Search Console sees.
- CrUX Dashboard — the Chrome User Experience Report in Looker Studio; useful for tracking trends over time.
Lab data (simulated):
- Lighthouse (in Chrome DevTools or PageSpeed Insights) — runs a simulated page load and diagnoses specific issues. Lab scores do not directly correlate with field scores, but the diagnostics point to real problems.
- WebPageTest — more detailed lab analysis with waterfall charts, filmstrips, and multi-location testing. Better than Lighthouse for diagnosing complex performance issues.
Rule of thumb: use field data to confirm a problem exists and track improvement. Use lab tools to find the cause.
LCP — Largest Contentful Paint
LCP measures how long it takes for the largest visible element to render. Target: under 2.5 seconds.
The LCP element is usually a hero image, a large heading, or an above-the-fold video poster. Identify it with PageSpeed Insights or by hovering over the LCP metric in Chrome DevTools Performance panel.
1. Optimise the LCP image
If the LCP element is an image (the most common case):
Format. Serve WebP or AVIF instead of JPEG or PNG. WebP is typically 25–35% smaller at equivalent quality; AVIF is smaller still but has slightly lower browser support. Use <picture> with fallbacks:
<picture>
<source srcset="hero.avif" type="image/avif">
<source srcset="hero.webp" type="image/webp">
<img src="hero.jpg" alt="..." width="1200" height="630">
</picture>
Size. Serve the image at the size it will be displayed, not larger. A hero image displayed at 800px wide does not need to be 2400px wide. Use srcset to serve different sizes at different breakpoints.
Compression. Run images through Squoosh, Sharp, or a CDN image optimisation service. Aim for the smallest file size that does not produce visible artefacts.
Priority loading. The LCP image must not be lazy-loaded. Add fetchpriority="high" to the LCP image element and ensure it is not set to loading="lazy":
<img src="hero.webp" fetchpriority="high" loading="eager" alt="...">
Preload. For images loaded via CSS background or JavaScript, add a preload link in the <head>:
<link rel="preload" as="image" href="hero.webp" fetchpriority="high">
2. Improve server response time (TTFB)
Time to First Byte is the time the server takes to respond. Slow TTFB delays everything, including LCP. Target TTFB under 800ms.
Caching. Cache HTML responses at the CDN or server level. Static pages should be served from cache, not regenerated on every request.
CDN. Serve content from a CDN with edge nodes close to users. Without a CDN, users far from your origin server see high latency.
Server resources. Underpowered hosting is a common cause of slow TTFB on WordPress and other dynamic CMSs. Evaluate whether your hosting tier matches your traffic level.
Database queries. On dynamic sites, slow database queries inflate TTFB. Profile queries using your CMS’s debugging tools and add indexes or caching where needed.
3. Eliminate render-blocking resources
Render-blocking resources are CSS and JavaScript files in the <head> that prevent the browser from rendering anything until they are downloaded and processed.
CSS: critical CSS (the styles needed for above-the-fold content) should be inlined in the <head>. Non-critical CSS can be loaded asynchronously.
JavaScript: add defer or async to scripts that are not needed for the initial render. defer executes after HTML parsing, in order. async executes as soon as downloaded, in any order.
<script src="analytics.js" defer></script>
Avoid large third-party scripts (chat widgets, tag managers, ad scripts) loading synchronously above the fold. Load them after the page renders.
4. Check for client-side rendering
If LCP content is rendered by JavaScript (React, Vue, Angular, etc.), the browser must download, parse, and execute the JavaScript before the LCP element appears. This adds significant time.
Solutions in order of effectiveness:
- Server-side rendering (SSR) — render HTML on the server so content is present on first byte
- Static site generation (SSG) — pre-render at build time
- Streaming SSR — send HTML in chunks as it is rendered server-side
- Prerendering — render a static snapshot for bots and slow connections
INP — Interaction to Next Paint
INP measures the latency of all interactions (clicks, taps, keyboard input) during a page visit, reporting the worst one. Target: under 200ms.
INP replaced FID (First Input Delay) in March 2024. FID only measured the first interaction; INP measures all of them throughout the page lifecycle.
1. Reduce JavaScript execution time
Long JavaScript tasks on the main thread delay interaction responses. The browser cannot respond to clicks while it is executing a task.
Identify long tasks in Chrome DevTools Performance panel — they appear as red-flagged tasks over 50ms.
Break up long tasks. Split large tasks using setTimeout, scheduler.yield(), or the Scheduler API:
async function processInChunks(items) {
for (const item of items) {
process(item);
await scheduler.yield(); // yields to the browser between items
}
}
Defer non-critical JavaScript. Any script not needed for user interaction should be deferred or loaded on demand. Third-party scripts (chat, analytics, ad networks) are frequent offenders.
2. Reduce input delay
Input delay is the time between a user interaction and when the browser begins processing it. It is caused by other tasks running on the main thread when the interaction occurs.
Common causes:
- Long-running timers (
setInterval,setTimeoutcallbacks doing heavy work) - Third-party scripts polling or firing events frequently
- Synchronous storage access (
localStorage,sessionStoragein a tight loop)
Audit third-party scripts. Load third-party scripts in a web worker or defer them until after the page is interactive. Use the Chrome DevTools Performance panel to identify which scripts are consuming main thread time.
3. Optimise event handlers
Heavy work in click or input event handlers directly increases processing time.
- Move expensive operations off the critical path — calculate or fetch data before the interaction where possible
- Use
requestAnimationFramefor visual updates - Debounce input handlers that fire on every keystroke
- Avoid synchronous DOM queries (e.g.,
offsetHeight,getBoundingClientRect) that force layout recalculation inside handlers
4. Reduce DOM size
Large DOMs (over ~1,500 nodes) slow down style calculations, layout, and JavaScript traversals. Every DOM operation takes longer.
- Remove unused HTML — hidden elements, noscript fallbacks, duplicate markup
- Virtualise long lists — only render the items visible in the viewport (libraries: Tanstack Virtual, react-window)
- Avoid deeply nested elements
CLS — Cumulative Layout Shift
CLS measures unexpected layout shifts — elements moving after the page has rendered. Target: under 0.1.
Layout shifts feel jarring to users and can cause accidental clicks. CLS is measured by the impact fraction (how much of the viewport shifted) multiplied by the distance fraction (how far elements moved).
1. Set explicit dimensions on images and videos
The most common CLS cause. If the browser does not know an image’s dimensions before it loads, it allocates no space for it. When the image loads, content shifts down.
Always declare width and height attributes on <img> elements. Modern browsers use these to reserve the correct space via the intrinsic aspect ratio, even before the image loads:
<img src="photo.webp" width="800" height="450" alt="...">
For responsive images, set height: auto in CSS to allow the width to scale while the aspect ratio is maintained.
For videos and iframes, use the aspect-ratio CSS property or the padding-top hack:
.video-wrapper {
aspect-ratio: 16 / 9;
}
2. Prevent font-related layout shift
Web fonts cause layout shift when the browser renders text with a fallback font, then swaps to the web font once it loads (FOUT — flash of unstyled text). The font metrics differ, causing text to reflow.
font-display: optional — the strictest approach. The browser uses the fallback if the font is not already cached. No layout shift, but web font may not display on first visit.
font-display: swap — shows fallback immediately, swaps when the web font loads. Can cause shift. Pair it with size-adjust, ascent-override, and descent-override in the @font-face declaration to match fallback metrics to the web font, minimising shift:
@font-face {
font-family: 'FallbackFont';
src: local('Arial');
ascent-override: 90%;
descent-override: 22%;
size-adjust: 107%;
}
Preload critical fonts to reduce the time the fallback is visible:
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
3. Reserve space for dynamic content
Banners, ads, cookie notices, and any content injected after load cause shift if they push existing content.
- Reserve space for ads with
min-heighton the container before the ad loads - Show cookie banners overlaid (fixed/sticky positioning) rather than pushing content
- Inject new content below the fold, not above existing content
4. Avoid inserting content above existing content
Any DOM insertion above existing visible content causes shift. If content must be added dynamically (infinite scroll, live updates), insert it below what is already visible or use skeleton screens that reserve the correct space before content loads.
5. Animations and transitions
CSS animations that change layout properties (top, left, width, height, margin) cause CLS. Use transform and opacity instead — they run on the compositor thread and do not cause layout shifts:
/* causes layout shift */
.element { transition: top 0.3s; }
/* does not cause layout shift */
.element { transition: transform 0.3s; }
Prioritisation
If you have limited development time, fix in this order:
- LCP image format, size, and fetchpriority — highest impact per effort
- TTFB / server caching — affects LCP and all subsequent metrics
- Image dimensions — most common CLS cause, low effort to fix
- Defer third-party scripts — improves both LCP and INP
- Long JavaScript tasks — higher effort, but necessary for INP on JS-heavy sites
Field data in Search Console updates with a 28-day rolling window. Changes to your site take up to four weeks to fully reflect in CWV reports.