Core Web Vitals became a Google ranking factor in June 2021. The metrics measure loading performance, interactivity, and visual stability. aspects that directly impact user experience and search rankings.
LCP measures how long it takes for the largest visible element to render. This is typically a hero image, heading, or large text block.
Thresholds:
Google's research shows sites with good LCP see 24% lower bounce rates.
INP replaced First Input Delay (FID) in March 2024. FID only measured the first interaction. INP measures all interactions throughout the page lifecycle. clicks, taps, keyboard presses.
Thresholds:
Chrome usage data shows users spend 90% of their time on a page after it loads, making responsiveness throughout the session more important than first-interaction metrics.
CLS measures visual stability. Every unexpected layout shift gets scored. images loading without dimensions, fonts causing text reflow, dynamic content insertion.
Thresholds:
Pure client-side rendering hurts LCP. The browser downloads JavaScript, parses it, executes it, then renders content. Vue's official performance guide states: "If your use case is sensitive to page load performance, avoid shipping it as a pure client-side SPA."
Use SSR or static site generation for content-heavy pages.
Preload the LCP element's resources in your index.html:
<head>
<link rel="preload" href="/hero-image.jpg" as="image">
<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin>
</head>
Load only what's visible on initial render:
<template>
<div>
<!-- Above-fold: eager loading -->
<img src="/hero.jpg" alt="Hero" width="1200" height="600">
<!-- Below-fold: lazy loading -->
<img
src="/product-1.jpg"
alt="Product"
width="800"
height="600"
loading="lazy"
>
</div>
</template>
Always specify width and height to prevent CLS.
Use font-display: swap to show system fonts immediately while web fonts load:
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var.woff2') format('woff2');
font-display: swap;
}
Split your bundle to load only what's needed:
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
component: () => import('./views/Home.vue')
},
{
path: '/dashboard',
component: () => import('./views/Dashboard.vue')
}
]
})
Vue.js parallel fetching improves LCP by loading component files, data, and images simultaneously.
Debouncing prevents performance issues from rapid-fire events:
<script setup>
import { ref } from 'vue'
const searchQuery = ref('')
let timeout = null
function handleSearch(event) {
clearTimeout(timeout)
timeout = setTimeout(() => {
searchQuery.value = event.target.value
// Perform search
}, 300)
}
</script>
<template>
<input placeholder="Search..." @input="handleSearch">
</template>
v-memo memoizes component output to skip re-renders:
<template>
<div v-for="item in items" :key="item.id" v-memo="[item.id, item.selected]">
{{ item.name }}
</div>
</template>
Re-renders only happen when item.id or item.selected changes.
Keep the main thread responsive:
// worker.ts
self.addEventListener('message', (e) => {
const result = heavyComputation(e.data)
self.postMessage(result)
})
// component.vue
const worker = new Worker(new URL('./worker.ts', import.meta.url))
worker.postMessage(data)
worker.addEventListener('message', (e) => {
console.log('Result:', e.data)
})
Computed properties cache results. Use them instead of methods for expensive operations:
<script setup>
import { computed, ref } from 'vue'
const items = ref([...largeArray])
// Cached, recalculates only when items changes
const filteredItems = computed(() =>
items.value.filter(item => item.active)
)
</script>
Breaking long tasks can reduce INP from 350ms to 120ms.
Prevent layout shifts by reserving space:
<template>
<img
src="/product.jpg"
alt="Product"
width="800"
height="600"
>
</template>
Or use aspect ratio with CSS:
<template>
<div class="image-container">
<img src="/product.jpg" alt="Product">
</div>
</template>
<style scoped>
.image-container {
aspect-ratio: 16 / 9;
overflow: hidden;
}
.image-container img {
width: 100%;
height: 100%;
object-fit: cover;
}
</style>
Skeleton screens or placeholders prevent shifts:
<script setup>
import { onMounted, ref } from 'vue'
const data = ref(null)
const loading = ref(true)
onMounted(async () => {
data.value = await fetchData()
loading.value = false
})
</script>
<template>
<div class="content">
<div v-if="loading" class="skeleton">
<!-- Same dimensions as actual content -->
</div>
<div v-else>
{{ data }}
</div>
</div>
</template>
<style scoped>
.skeleton {
height: 200px; /* Match real content height */
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
}
</style>
Preload fonts and use font-display: swap:
<head>
<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin>
</head>
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var.woff2') format('woff2');
font-display: swap;
size-adjust: 100%; /* Adjust fallback font metrics */
}
Adjusting local system fonts to match web fonts using fallback metrics reduces CLS.
Reserve space for third-party content:
<template>
<div class="ad-container" style="min-height: 250px;">
<!-- Ad loads here -->
</div>
</template>
PageSpeed Insights shows field data (real user metrics from Chrome UX Report) and lab data (simulated tests).
Field data is what Google uses for rankings. Lab data helps debug specific issues.
Run Lighthouse in Chrome DevTools (Performance tab) for local testing. It measures LCP, CLS, and provides optimization suggestions.
Record a session to see frame-by-frame rendering:
Chrome DevTools shows local LCP and CLS scores instantly. Interact with the page to capture INP.
Google's web-vitals library sends metrics to your analytics:
import { onCLS, onINP, onLCP } from 'web-vitals'
onLCP((metric) => {
console.log('LCP:', metric.value)
// Send to analytics
sendToAnalytics({ metric: 'LCP', value: metric.value })
})
onINP((metric) => {
console.log('INP:', metric.value)
sendToAnalytics({ metric: 'INP', value: metric.value })
})
onCLS((metric) => {
console.log('CLS:', metric.value)
sendToAnalytics({ metric: 'CLS', value: metric.value })
})
function sendToAnalytics({ metric, value }) {
// Send to Google Analytics, Plausible, etc.
if (window.gtag) {
window.gtag('event', metric, { value: Math.round(value) })
}
}
Capture p75 and p95 metrics for all critical flows, sliced by device and region.
Google treats Core Web Vitals as a tiebreaker ranking factor. Content relevance is still the primary signal. For queries with multiple relevant results, good page experience can be the differentiator.
Industry research shows Core Web Vitals account for 10–15% of ranking signals. Only 47% of websites pass all three metrics in 2025.
Sites passing all three vitals see:
Mobile-first indexing now treats Core Web Vitals as indexing requirements rather than just ranking signals. Google evaluates mobile versions of sites primarily.
To pass assessment, 75% of page visits must meet "Good" thresholds.
Nuxt provides built-in performance optimizations including automatic code splitting, image optimization, and hybrid rendering. See our Nuxt Core Web Vitals guide for framework-specific implementations.