Performance · · 6 min read

Fixing Core Web Vitals in a Nuxt app: LCP, CLS and INP

A practical guide to improving Largest Contentful Paint, Cumulative Layout Shift and Interaction to Next Paint in Vue and Nuxt apps.

By Mukesh Pensia, Vue.js & Nuxt developer

Core Web Vitals measure how fast a page loads, how stable it is and how quickly it responds. They feed into Google's page experience signals, and slow pages lose visitors before search even comes into it. Here is how we approach each metric in a Nuxt app.

The targets, measured at the 75th percentile of real visits:

  • LCP (Largest Contentful Paint): 2.5 s or less
  • CLS (Cumulative Layout Shift): 0.1 or less
  • INP (Interaction to Next Paint): 200 ms or less

Measure field data first

Lighthouse runs on one simulated device. Google ranks on field data from real Chrome users, shown in Search Console's Core Web Vitals report and in PageSpeed Insights. Start there to find which pages and which metric actually fail, then use Lighthouse and the Performance panel to debug them.

LCP: get the main content on screen sooner

The largest element is usually a hero image or heading. For images, load it early and at the right size:

<NuxtImg
  src="/hero.jpg"
  width="1200"
  height="630"
  sizes="100vw md:1200px"
  preload
  fetchpriority="high"
/>
  • Never lazy-load the LCP image. Lazy-load images below the fold with loading="lazy".
  • Prerender or cache marketing pages with routeRules so the HTML arrives fast.
  • Audit plugins and global CSS, because everything loaded before the first render delays LCP.

CLS: stop the page from jumping

  • Always set width and height (or aspect-ratio) on images, videos and embeds.
  • Reserve space for banners, cookie bars and anything injected after load.
  • Web fonts swapping in cause shifts. The @nuxt/fonts module self-hosts fonts and generates metric-matched fallbacks, so the swap barely moves the text.
  • Avoid client-only components above the fold that render nothing on the server and then pop in.

INP: keep the main thread free

INP suffers when JavaScript blocks the main thread just as the user taps something. In a Nuxt app the usual culprits are hydration work, third-party scripts and large reactive data.

Delay hydrating components the user doesn't need yet (Nuxt 3.16 and later):

<LazyCommentsSection hydrate-on-visible />
<LazyNewsletterForm hydrate-on-interaction />

Load third-party scripts after the app is ready with @nuxt/scripts:

useScript('https://widget.example.com/chat.js', {
  trigger: 'onNuxtReady',
})
  • Use shallowRef for large arrays and API responses you replace rather than mutate deeply.
  • Virtualise long lists instead of rendering thousands of rows.
  • Debounce search inputs and move heavy work out of input handlers.

Check again after release

Field data takes about 28 days to fully reflect a change, so compare the Search Console report before and after. A quick lab score is a good sign, but the real-user numbers are what count.

Need a hand with your Nuxt app?

More notes