/ โ† Core Web Vitals Fixes / Next.js and Nuxt
In this guide
  1. Which framework decision affects which metric
  2. Choosing a rendering mode per route
  3. The fix, step by step
  4. Framework code for images, fonts and scripts
  5. Hydration and INP
  6. Edge cases
  7. Keeping framework sites fast
  8. Questions and answers

Which framework decision affects which metric

DecisionMetric it movesWhat goes wrong
Rendering mode per routeTTFB, then LCPA page that is the same for everyone is rendered on the server for every request, or worse, rendered only in the browser.
Client versus server componentsINP, and LCP on slower phonesA 'use client' directive near the top of the tree sends most of the page's code to the browser to hydrate.
Image component usageLCP and CLSHero images lazy-loaded by default, raw img tags without dimensions, or fill images in containers with no height.
Font loadingCLS and LCPFonts linked from an external stylesheet in the document head instead of through the framework's font tooling.
Third-party scriptsINP, and LCP through bandwidthAnalytics, tag managers and chat added with a plain script tag that loads during hydration.
Data fetchingTTFB and LCPOne slow API call holds back the whole server response.

The thresholds are the same as everywhere: LCP 2.5 seconds or less, INP 200 milliseconds or less and CLS 0.1 or less, at the 75th percentile of real visits. Framework sites often pass LCP comfortably on desktop and fail INP on mobile, because the cost of hydration lands on the visitor's processor.

๐Ÿ’ก A client-rendered page, where the server sends an almost empty HTML shell and JavaScript builds everything, is the worst case for LCP and for search engines. Content and marketing routes should always send their main content in the HTML.

Choosing a rendering mode per route

Both frameworks let you decide per route whether HTML is built once at build time, rebuilt in the background on a timer, or built fresh for each request. The right answer depends on how often the content changes and whether it differs between visitors.

Route typeGood choiceWhy
Home, service pages, articles, documentationStatic at build timeSame for everyone and changes rarely, so HTML can come straight from a CDN.
Product and category pages, listings that change during the dayIncremental: static with background revalidationServed from cache like a static page, refreshed on a schedule or when content changes.
Search results, personalised pagesServer rendered per request, streamedDepends on the request, but the main content still arrives in HTML.
Account dashboards behind a loginServer or client renderedNot indexed, and visitors are already engaged; still keep INP in mind.

Next.js App Router

// app/products/[slug]/page.tsx
export const revalidate = 3600;   // rebuild at most once an hour

export async function generateStaticParams() {
  const products = await getTopProducts();
  return products.map(p => ({ slug: p.slug }));
}

export default async function ProductPage({ params }) {
  const { slug } = await params;
  const product = await getProduct(slug);
  return <ProductView product={product} />;
}

Watch out for things that silently switch a route to per-request rendering, such as reading cookies or headers in a layout shared by every page. Check the build output, which marks each route as static or dynamic, after every significant change. Caching defaults have changed between major Next.js versions, so confirm the behaviour in the documentation for the version you run.

Nuxt

// nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    '/': { prerender: true },
    '/about': { prerender: true },
    '/blog/**': { isr: 3600 },
    '/products/**': { swr: 600 },
    '/account/**': { ssr: false }
  }
});

How isr and swr behave depends on where you deploy, since the cache lives in the hosting platform or in Nuxt's server. Check the response headers on a deployed page to confirm pages are actually being served from cache.

The fix, step by step

Step 1

Measure each route with real-user reporting

Run one URL per route type through the Core Web Vitals checker and note which metric fails on mobile and desktop. Then add real-user reporting: in Next.js the useReportWebVitals hook from next/web-vitals, and in Nuxt the web-vitals library in a client plugin. Send the route pattern with each result rather than the full URL, so you can see which route type fails. Single-page navigations are reported differently from full loads, so record both.

Step 2

Choose the rendering mode per route

For each route, decide whether its HTML can be built once, refreshed on a schedule, or must be built per request. Move content pages to static generation, listings and products to incremental regeneration with a sensible revalidate time, and keep per-request rendering for search and personalised pages only. In Next.js check the build output for routes that became dynamic by accident. In Nuxt set the choice in routeRules and confirm cache headers on the deployed site.

Step 3

Keep components on the server by default

In the Next.js App Router, components are server components unless marked with 'use client'. Search for that directive and push each one as far down the tree as possible, so only the interactive button, form or menu becomes client code rather than the whole page or layout. Pass server-rendered content into client components as children. In Nuxt, avoid wrapping static content in components that need client state, and use lazy components for widgets.

Step 4

Give the hero image priority through the image component

Replace raw img tags with next/image or NuxtImg, always with width and height, or with fill inside a container that has a set aspect ratio. The image components lazy-load by default, which is wrong for the LCP image, so mark the hero as eager and high priority using the prop your framework version provides. Give it an accurate sizes value so phones do not download the desktop file.

Step 5

Load fonts through the framework font tools

Remove font stylesheet links from the document head and load fonts through next/font in Next.js or the Nuxt Fonts module in Nuxt. Both download the font files at build time and serve them from your own domain, and both can generate a fallback font with adjusted metrics so text does not reflow when the web font arrives. Load only the weights and subsets you use, and apply the font once in the root layout.

Step 6

Defer third-party scripts and client-only widgets

Add analytics, tag managers and chat through next/script with the afterInteractive or lazyOnload strategy, or through the Nuxt Scripts module, rather than plain script tags. Load heavy client-only widgets such as maps, carousels and review panels with next/dynamic or Nuxt lazy components, and only when they are about to be seen or opened. This keeps hydration short, so the page responds sooner to the first tap.

Step 7

Stream slow data instead of blocking the page

If a route must render per request, do not let one slow API call hold back the whole response. In the Next.js App Router, wrap slow sections in Suspense with a fallback of the same size, or add a loading file for the route, so the page shell and main content are sent first. In Nuxt, fetch non-essential data with lazy: true so it does not block rendering. Reserve space for streamed sections to avoid layout shift.

Step 8

Check the bundle and hydration cost before release

Run your framework's bundle analyser and look for large libraries in the shared client bundle, such as date, chart or icon packages imported whole. Record a page load and a few interactions in DevTools with a 4x CPU slowdown and check the hydration task and any long tasks after it. Deploy, watch the route-level real-user results, and allow 28 days for the Chrome UX Report and Search Console to reflect the change.

Framework code for images, fonts and scripts

Hero image and font in Next.js

// app/layout.tsx
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'], display: 'swap' });

export default function RootLayout({ children }) {
  return (
    <html lang="en-GB" className={inter.className}>
      <body>{children}</body>
    </html>
  );
}

// app/page.tsx
import Image from 'next/image';
import hero from './hero-kitchen.jpg';

export default function Home() {
  return (
    <Image
      src={hero}
      alt="Fitted oak kitchen with island"
      sizes="100vw"
      loading="eager"
      fetchPriority="high"
    />
  );
}

A statically imported image gives next/image its width and height automatically. Older versions use a priority prop for the hero, and newer versions have changed this prop, so check the image documentation for your version; setting eager loading and high fetch priority directly works as shown.

Third-party script in Next.js

import Script from 'next/script';

<Script src="https://widgets.example.com/chat.js" strategy="lazyOnload" />

Hero image and a lazy widget in Nuxt

<template>
  <NuxtImg
    src="/img/hero-kitchen.jpg"
    width="1600" height="900"
    sizes="100vw md:100vw lg:1600px"
    alt="Fitted oak kitchen with island"
    loading="eager"
    fetchpriority="high"
    preload
  />
  <LazyReviewCarousel v-if="showReviews" />
</template>

Prefixing a component name with Lazy makes Nuxt split it into its own chunk and load it only when it is rendered. Recent Nuxt versions can also delay hydration of a component until it is visible or the browser is idle, which is useful for heavy widgets further down the page.

Pushing the client boundary down

// Before: the whole product page is client code
'use client';
export default function ProductPage() { /* ... */ }

// After: page stays on the server, only the button hydrates
// app/products/[slug]/page.tsx (server)
import AddToBasket from './AddToBasket';
export default async function ProductPage({ params }) {
  const { slug } = await params;
  const product = await getProduct(slug);
  return (
    <article>
      <h1>{product.name}</h1>
      <ProductDetails product={product} />
      <AddToBasket id={product.id} />
    </article>
  );
}

// app/products/[slug]/AddToBasket.tsx
'use client';
export default function AddToBasket({ id }) { /* state and handlers */ }

Hydration and INP

Hydration is the step where the framework runs in the browser, attaches event handlers and makes server-rendered HTML interactive. Until it finishes, taps may not work, and while it runs it occupies the main thread in long tasks. On a mid-range phone, hydrating a large page can take long enough that a visitor's first tap waits, which shows up as input delay in INP.

โš ๏ธ In the Next.js App Router, next/dynamic with ssr: false can only be used inside a client component. Using it to skip server rendering of main content also removes that content from the HTML, which hurts LCP and indexing. Keep it for widgets that genuinely only work in the browser.

Edge cases

Keeping framework sites fast

For the individual metrics, see poor INP, slow TTFB and CLS in React and Next.js. Image specifics are in images in Next.js.

Questions and answers

1. Are Next.js and Nuxt sites fast by default?

They start from a good position, with server rendering, automatic code splitting and image and font components, but defaults do not guarantee a pass. Sites commonly fail Core Web Vitals by rendering pages per request that could be static, marking large parts of the component tree as client code, adding third-party scripts with plain script tags, and using images without dimensions. The framework gives you the tools; each route still needs the right choices.

2. Should I use static generation or server rendering?

Use static generation for pages that are the same for every visitor and change rarely, such as the home page, service pages and articles, because the HTML can be served from a CDN with a very fast response. Use incremental regeneration for pages that change during the day, such as products and listings. Keep per-request server rendering for search results and personalised pages. Choose per route, not once for the whole site, and review the choice when a page type changes.

3. Does 'use client' make a page slower?

It sends that component and everything it imports to the browser, where it has to be downloaded, parsed and hydrated. Placed at the top of a page or layout, it turns the whole tree below into client code, which increases JavaScript and hydration time and can hurt INP on phones. The page is still rendered on the server first, so content appears in the HTML, but interactivity arrives later. Put the directive only on interactive components.

4. Why is my Next.js hero image hurting LCP?

The image component lazy-loads by default, which delays the most important image on the page. Mark the hero image as eager with high fetch priority, using the prop your Next.js version supports, and give it an accurate sizes value so phones download a suitably small file. Also check that the hero is not loaded inside a client-only component or behind a carousel, which would delay it until JavaScript runs. Test the result at a phone width with throttling.

5. Is next/font better than loading Google Fonts with a link tag?

Usually yes. next/font downloads the font files at build time and serves them from your own domain, removing the connection to an external font service. It also generates a fallback font with adjusted size metrics, which reduces the layout shift when the web font replaces the fallback. Nuxt's Fonts module does similar work for Nuxt sites. Limit the weights and subsets you load either way, since each extra file adds download time.

6. How do I add Google Analytics or a tag manager without hurting INP?

Load them through the framework rather than with plain script tags: next/script with the afterInteractive or lazyOnload strategy in Next.js, the official Next.js third-parties package, or the Nuxt Scripts module in Nuxt. This stops them loading before your page is interactive. Keep the number of tags small, since each still runs on the main thread, and check a throttled DevTools recording for long tasks owned by tag scripts after adding one.

7. Why does my site pass in the lab but fail INP in the field?

Lab tests load the page once and do not interact with it, so they miss slow handlers and hydration delays that real visitors hit when they tap early. Real visitors are also often on slower phones than the simulated device. Add route-level real-user reporting with the web-vitals library or the framework's reporting hook, record the interaction target and phases, and reproduce the slowest interactions in DevTools with CPU throttling switched on.

Test your framework routes

The Core Web Vitals checker shows real-user field data and a Lighthouse lab test side by side for your URL, breaks down each metric and points to the assets and code behind every failure, so you can see which route type and which bundles need work.

Open Core Web Vitals Fixes โ†’
๐Ÿ’ฌ Got a problem?
Related Guides: All Core Web Vitals Fixes  ·  Next.js Page Speed  ·  Images in Next.js  ·  CLS in React and Next.js  ·  Learning Hub

About aiwebpageseo

aiwebpageseo.com is a data-driven SEO and AEO (Answer Engine Optimisation) platform providing a free suite of technical SEO tools. Rather than relying on AI-theorised assumptions, the platform analyses live URL performance, delivering objective diagnostics, page speed metrics, CLS debugging, and site crawl data alongside actionable technical tutorials.