Core Web Vitals for Vue Applications

Measure and optimize LCP, INP, and CLS in Vue apps to improve user experience and search rankings.
Harlan WiltonHarlan Wilton12 mins read Published
What you'll learn
  • Target thresholds: LCP ≤2.5s, INP ≤200ms, CLS ≤0.1
  • Core Web Vitals account for 10-15% of ranking signals. only 47% of sites pass all three
  • Use PageSpeed Insights for field data (what Google uses) and Lighthouse for debugging

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.

The Three Metrics

LCP (Largest Contentful Paint)

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:

  • Good: ≤ 2.5 seconds
  • Needs improvement: 2.5–4.0 seconds
  • Poor: > 4.0 seconds

Google's research shows sites with good LCP see 24% lower bounce rates.

INP (Interaction to Next Paint)

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:

  • Good: ≤ 200 milliseconds
  • Needs improvement: 200–500 milliseconds
  • Poor: > 500 milliseconds

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 (Cumulative Layout Shift)

CLS measures visual stability. Every unexpected layout shift gets scored. images loading without dimensions, fonts causing text reflow, dynamic content insertion.

Thresholds:

  • Good: ≤ 0.1
  • Needs improvement: 0.1–0.25
  • Poor: > 0.25

Vue LCP Optimizations

Avoid Client-Side Rendering for Content

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 Critical Assets

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>

Lazy Load Below-Fold Images

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.

Optimize Font Loading

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;
}

Dynamic Imports for Route Components

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.

Vue INP Optimizations

Debounce Event Handlers

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>

Use v-memo for Large Lists

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.

Offload Heavy Computation to Web Workers

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)
})

Optimize Computed Properties

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.

Vue CLS Fixes

Set Image Dimensions

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>

Reserve Space for Dynamic Content

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>

Avoid Flash of Unstyled Content (FOUC)

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.

Handle Ads and Embeds

Reserve space for third-party content:

<template>
  <div class="ad-container" style="min-height: 250px;">
    <!-- Ad loads here -->
  </div>
</template>

Measuring Core Web Vitals

PageSpeed Insights

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.

Lighthouse

Run Lighthouse in Chrome DevTools (Performance tab) for local testing. It measures LCP, CLS, and provides optimization suggestions.

Chrome DevTools Performance Panel

Record a session to see frame-by-frame rendering:

  1. Open DevTools → Performance
  2. Click Record
  3. Interact with your page
  4. Stop recording
  5. View LCP, CLS, and long tasks

Chrome DevTools shows local LCP and CLS scores instantly. Interact with the page to capture INP.

web-vitals Library

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.

Core Web Vitals Impact on Rankings

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:

  • 24% lower bounce rates
  • 25% higher conversion rates moving from Poor to Good
  • 30% revenue improvements from higher engagement

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.

Using Nuxt?

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.