@nuxt/image with priority prop for above-fold images, loading="lazy" for below-foldCore 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:
Nuxt renders pages on the server by default, which significantly improves LCP compared to client-side rendering. The browser receives fully rendered HTML, eliminating the wait for JavaScript execution before content appears.
No configuration needed. Nuxt handles this automatically.
Install the Nuxt Image module for automatic image optimization:
npx nuxi@latest module add image
Use <NuxtImg> and <NuxtPicture> for optimized delivery:
<template>
<div>
<!-- Above-fold: prioritize LCP image -->
<NuxtImg
src="/hero.jpg"
alt="Hero"
width="1200"
height="600"
priority
/>
<!-- Below-fold: lazy loading -->
<NuxtImg
src="/product-1.jpg"
alt="Product"
width="800"
height="600"
loading="lazy"
/>
</div>
</template>
The priority prop preloads the LCP image and disables lazy loading. @nuxt/image automatically generates responsive sizes, modern formats (WebP/AVIF), and optimizes delivery.
Add preload hints in your page or layout:
<script setup>
useHead({
link: [
{
rel: 'preload',
href: '/fonts/inter-var.woff2',
as: 'font',
type: 'font/woff2',
crossorigin: ''
}
]
})
</script>
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;
}
Configure global font preloading in nuxt.config.ts:
export default defineNuxtConfig({
app: {
head: {
link: [
{
rel: 'preload',
href: '/fonts/inter-var.woff2',
as: 'font',
type: 'font/woff2',
crossorigin: ''
}
]
}
}
})
Nuxt automatically splits code at the route level. Each page in /pages/ becomes a separate chunk, loading only what's needed:
pages/
index.vue → Chunk: index.vue
dashboard.vue → Chunk: dashboard.vue
blog/
[slug].vue → Chunk: blog-[slug].vue
No configuration required. Nuxt handles this automatically during build.
For component-level splitting, use lazy loading:
<script setup>
// Lazy load heavy components
const LazyChart = defineAsyncComponent(() => import('~/components/Chart.vue'))
</script>
<template>
<div>
<LazyChart v-if="showChart" />
</div>
</template>
Prevent 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>
<NuxtImg
src="/product.jpg"
alt="Product"
width="800"
height="600"
/>
</template>
Or use aspect ratio with CSS:
<template>
<div class="image-container">
<NuxtImg 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>
const { data, pending } = await useFetch('/api/data')
</script>
<template>
<div class="content">
<div v-if="pending" 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:
// nuxt.config.ts
export default defineNuxtConfig({
app: {
head: {
link: [
{
rel: 'preload',
href: '/fonts/inter-var.woff2',
as: 'font',
type: 'font/woff2',
crossorigin: ''
}
]
}
}
})
@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.
What percentage of page visits must pass Core Web Vitals thresholds for a 'Good' rating?
50%: That's the threshold for "Needs Improvement", not "Good"75%: Correct! 75% of visits must have LCP ≤2.5s, INP ≤200ms, and CLS ≤0.1100%: Google uses the 75th percentile, not 100%, to account for edge cases and slow connections