Skip to main content
Back to Blog
Marketing & SEO

Next.js SEO: Core Web Vitals That Actually Matter

Master the technical SEO fundamentals for Next.js apps. Learn how to optimize Core Web Vitals, implement metadata, and build a search-engine-friendly application.

High Mountain Studio
12 min read
Dashboard showing Core Web Vitals metrics with green performance scores

Technical SEO for Next.js: Core Web Vitals That Actually Matter

Google doesn't just read your content. It measures how your site performs. Since 2021, Core Web Vitals have been a ranking factor, and in 2024, Google updated the metrics to focus even more on real-world user experience.

The good news? Next.js is built for performance. The framework handles many SEO fundamentals out of the box. But "built for performance" doesn't mean "automatically optimized." You still need to understand what matters, measure it correctly, and make intentional choices.

Let's cut through the noise and focus on the technical SEO that actually moves the needle for Next.js applications.

Understanding Core Web Vitals

Core Web Vitals are Google's metrics for measuring user experience. In 2024, there are three primary metrics:

1. Largest Contentful Paint (LCP)

What it measures: Loading performance. Specifically, how long it takes for the largest content element to appear.

Target: Under 2.5 seconds

What counts as LCP:

  • Images (including background images)
  • Video poster images
  • Block-level text elements
  • SVGs
Good:    ≤ 2.5s
Needs improvement: 2.5s - 4.0s
Poor:    > 4.0s

2. Interaction to Next Paint (INP)

What it measures: Responsiveness. How quickly the page responds to user interactions throughout the entire session.

Target: Under 200 milliseconds

INP replaced First Input Delay (FID) in March 2024. Unlike FID (which only measured the first interaction), INP measures all interactions and reports the worst one (with some statistical smoothing).

Good:    ≤ 200ms
Needs improvement: 200ms - 500ms
Poor:    > 500ms

3. Cumulative Layout Shift (CLS)

What it measures: Visual stability. How much the page content shifts unexpectedly during loading.

Target: Under 0.1

CLS is a score, not a time measurement. A score of 0 means nothing shifted; higher numbers indicate more jarring experiences.

Good:    ≤ 0.1
Needs improvement: 0.1 - 0.25
Poor:    > 0.25

Optimizing LCP in Next.js

LCP is often the most impactful metric to improve. Here's how to tackle it in Next.js.

Identify Your LCP Element

First, know what you're optimizing. Use Chrome DevTools:

  1. Open DevTools → Performance tab
  2. Click Record, load your page, stop recording
  3. Click on the LCP event in the timeline
  4. Check "Related Node" to see what element is your LCP

Common LCP elements:

  • Hero images
  • Featured product images
  • Blog post featured images
  • Large headline text (if no images above fold)

Optimize Images with next/image

The next/image component is your primary LCP optimization tool:

import Image from 'next/image';

export function HeroSection() {
  return (
    <section>
      <Image
        src="/hero-image.jpg"
        alt="Descriptive alt text for SEO"
        width={1200}
        height={600}
        priority // Preloads this image
        sizes="100vw" // Full-width image
        placeholder="blur" // Shows blur while loading
        blurDataURL="data:image/jpeg;base64,..."
      />
      <h1>Your Headline Here</h1>
    </section>
  );
}

Key props for LCP optimization:

| Prop | Purpose | |------|---------| | priority | Preloads image, disables lazy loading | | sizes | Helps browser pick correct image size | | placeholder="blur" | Prevents layout shift, improves perceived performance |

Critical rule: Only one image per page should have priority: your LCP element.

Server Components for Faster HTML

Next.js 15's Server Components send HTML immediately without waiting for JavaScript. For LCP text elements, this is huge:

// This component renders on the server
// HTML arrives with content already present
export default async function HeroHeadline() {
  return (
    <h1 className="text-5xl font-bold">
      Ship products 3x faster
    </h1>
  );
}

No JavaScript bundle needs to download before this text appears.

Font Optimization

Fonts can delay text rendering. Next.js has built-in font optimization:

// app/layout.tsx
import { Inter } from 'next/font/google';

const inter = Inter({
  subsets: ['latin'],
  display: 'swap', // Prevents invisible text
  preload: true,
});

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

The display: 'swap' ensures text appears immediately with a fallback font, then swaps to the custom font when loaded.

Optimizing INP in Next.js

INP measures how responsive your page feels. Poor INP usually means JavaScript is blocking the main thread during interactions.

Minimize Client-Side JavaScript

The biggest INP win is shipping less JavaScript. Next.js 15 helps with Server Components:

// ❌ Client Component - ships JavaScript
'use client';
import { useState } from 'react';
import { formatDate } from 'date-fns';

export function ArticleCard({ article }) {
  const [expanded, setExpanded] = useState(false);
  
  return (
    <div>
      <h2>{article.title}</h2>
      <time>{formatDate(article.date, 'PPP')}</time>
      {/* Interactive parts */}
    </div>
  );
}

// ✅ Server Component with minimal client interactivity
import { formatDate } from 'date-fns';
import { ExpandButton } from '@/components/expand-button';

export function ArticleCard({ article }) {
  return (
    <div>
      <h2>{article.title}</h2>
      <time>{formatDate(article.date, 'PPP')}</time>
      <ExpandButton /> {/* Only this tiny part is client */}
    </div>
  );
}

The date-fns library never ships to the client in the second example.

Avoid Long Tasks

JavaScript tasks over 50ms are "long tasks" that block interactions. Break them up:

// ❌ Long synchronous operation
function processAllItems(items) {
  return items.map(item => expensiveOperation(item));
}

// ✅ Yield to main thread periodically
async function processAllItemsAsync(items) {
  const results = [];
  
  for (const item of items) {
    results.push(expensiveOperation(item));
    
    // Yield every 10 items
    if (results.length % 10 === 0) {
      await new Promise(resolve => setTimeout(resolve, 0));
    }
  }
  
  return results;
}

Use Concurrent Features

React 18's concurrent features help keep your UI responsive:

'use client';
import { useTransition, useState } from 'react';

export function SearchFilter({ onFilter }) {
  const [isPending, startTransition] = useTransition();
  const [query, setQuery] = useState('');

  function handleChange(e) {
    const value = e.target.value;
    setQuery(value); // Urgent: update input immediately
    
    startTransition(() => {
      onFilter(value); // Non-urgent: can be interrupted
    });
  }

  return (
    <input 
      value={query}
      onChange={handleChange}
      className={isPending ? 'opacity-50' : ''}
    />
  );
}

The startTransition marks the filtering as non-urgent, so typing remains snappy.

Optimizing CLS in Next.js

Layout shift is frustrating for users and often easy to fix.

Reserve Space for Images

Always specify dimensions for images:

// ❌ No dimensions - causes layout shift
<img src="/photo.jpg" alt="..." />

// ✅ Dimensions prevent shift
<Image
  src="/photo.jpg"
  alt="..."
  width={800}
  height={600}
/>

The next/image component requires dimensions, which is a feature, not a limitation.

Handle Dynamic Content

Content that appears after initial render (like ads, embeds, or async data) needs reserved space:

// Reserve space for dynamic content
export function AdSlot() {
  return (
    <div 
      className="min-h-[250px] bg-gray-100"
      aria-hidden="true"
    >
      {/* Ad loads here */}
    </div>
  );
}

// Reserve space for async content
export function UserGreeting() {
  const user = useUser(); // May take time to load

  return (
    <div className="h-10"> {/* Fixed height prevents shift */}
      {user ? `Hello, ${user.name}` : <Skeleton />}
    </div>
  );
}

Font Loading Strategy

Fonts can cause text to reflow. Use font-display: swap with fallback that matches your custom font's metrics:

import { Roboto } from 'next/font/google';

const roboto = Roboto({
  subsets: ['latin'],
  display: 'swap',
  // Adjust fallback metrics to minimize shift
  adjustFontFallback: true,
});

Next.js automatically adjusts fallback font metrics to reduce layout shift.

Metadata and Structured Data

Beyond Core Web Vitals, proper metadata is essential for SEO. You can use our meta tag generator to create optimized Open Graph and Twitter Card tags for your pages.

Metadata API in Next.js 15

Use the built-in Metadata API:

// app/layout.tsx - Global metadata
import type { Metadata } from 'next';

export const metadata: Metadata = {
  metadataBase: new URL('https://yourdomain.com'),
  title: {
    default: 'Your Site Name',
    template: '%s | Your Site Name',
  },
  description: 'Default site description for SEO.',
  openGraph: {
    type: 'website',
    locale: 'en_US',
    siteName: 'Your Site Name',
  },
  twitter: {
    card: 'summary_large_image',
    creator: '@yourhandle',
  },
  robots: {
    index: true,
    follow: true,
  },
};
// app/blog/[slug]/page.tsx - Page-specific metadata
import type { Metadata } from 'next';

export async function generateMetadata({ params }): Promise<Metadata> {
  const post = await getPost(params.slug);
  
  return {
    title: post.title, // Becomes "Post Title | Your Site Name"
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [post.featuredImage],
      type: 'article',
      publishedTime: post.publishedAt,
      authors: [post.author],
    },
  };
}

JSON-LD Structured Data

Help search engines understand your content with structured data:

// components/structured-data.tsx
export function ArticleStructuredData({ article }) {
  const structuredData = {
    '@context': 'https://schema.org',
    '@type': 'Article',
    headline: article.title,
    description: article.excerpt,
    image: article.featuredImage,
    author: {
      '@type': 'Organization',
      name: 'Your Company',
    },
    publisher: {
      '@type': 'Organization',
      name: 'Your Company',
      logo: {
        '@type': 'ImageObject',
        url: 'https://yourdomain.com/logo.png',
      },
    },
    datePublished: article.publishedAt,
    dateModified: article.updatedAt,
  };

  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
    />
  );
}

Common structured data types:

  • Article: Blog posts, news articles
  • Product: E-commerce products
  • FAQPage: FAQ sections
  • Organization: Company info
  • BreadcrumbList: Navigation breadcrumbs

Technical SEO Checklist

Site Structure

  • [ ] Clean URL structure (/blog/post-slug, not /blog?id=123)
  • [ ] Proper use of robots.txt (you can use our free robots.txt generator to create one)
  • [ ] XML sitemap at /sitemap.xml (generate one with our sitemap generator tool)
  • [ ] Canonical URLs on all pages

On-Page Elements

  • [ ] Unique <title> per page (50-60 characters)
  • [ ] Unique <meta name="description"> per page (150-160 characters)
  • [ ] Proper heading hierarchy (one <h1> per page)
  • [ ] Descriptive image alt attributes
  • [ ] Internal linking between related content

Technical Foundation

  • [ ] HTTPS everywhere (no mixed content)
  • [ ] Mobile-responsive design
  • [ ] Fast load times (LCP < 2.5s)
  • [ ] No JavaScript required for critical content

Sitemap Generation

Next.js 15 makes sitemap generation straightforward:

// app/sitemap.ts
import type { MetadataRoute } from 'next';

export default async function sitemap(): MetadataRoute.Sitemap {
  const posts = await getAllPosts();
  
  const postUrls = posts.map(post => ({
    url: `https://yourdomain.com/blog/${post.slug}`,
    lastModified: post.updatedAt,
    changeFrequency: 'weekly' as const,
    priority: 0.8,
  }));

  return [
    {
      url: 'https://yourdomain.com',
      lastModified: new Date(),
      changeFrequency: 'daily',
      priority: 1,
    },
    {
      url: 'https://yourdomain.com/about',
      lastModified: new Date(),
      changeFrequency: 'monthly',
      priority: 0.5,
    },
    ...postUrls,
  ];
}

Measuring and Monitoring

You can't improve what you don't measure.

Tools for Measuring

Lab data (synthetic tests):

  • Lighthouse (Chrome DevTools, PageSpeed Insights)
  • WebPageTest
  • Chrome User Experience Report (CrUX)

Field data (real users):

  • Google Search Console
  • Vercel Analytics (built into Vercel deployments)
  • Custom Real User Monitoring (RUM)

Setting Up Real User Monitoring

Next.js has built-in support for reporting Web Vitals:

// app/layout.tsx
import { SpeedInsights } from '@vercel/speed-insights/next';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        <SpeedInsights />
      </body>
    </html>
  );
}

Or implement custom reporting:

// app/components/web-vitals.tsx
'use client';

import { useReportWebVitals } from 'next/web-vitals';

export function WebVitals() {
  useReportWebVitals((metric) => {
    // Send to your analytics
    console.log(metric.name, metric.value);
    
    // Or send to an API
    fetch('/api/analytics', {
      method: 'POST',
      body: JSON.stringify(metric),
    });
  });

  return null;
}

Quick Wins to Implement Today

  1. Add priority to your hero image: Instant LCP improvement
  2. Check for missing image dimensions: Run Lighthouse, fix CLS issues
  3. Review your fonts: Ensure display: swap is set
  4. Audit client components: Can any be converted to Server Components?
  5. Set up monitoring: Add Vercel Speed Insights or custom RUM

Need Help With Technical SEO?

Technical SEO for Next.js applications requires understanding both the framework's capabilities and search engine requirements. Get it right, and you build a foundation for organic growth. Get it wrong, and you're invisible.

At High Mountain Studio, we build Next.js applications with SEO baked in from day one, not bolted on at the end. Schedule a consultation to discuss your project's performance and SEO strategy.

Technical SEOCore Web VitalsNext.jsLCPFIDCLSPage SpeedGoogle RankingReact SEOVercelPerformance Optimization