教程:使用 Next.js App Router 优化 Core Web Vitals

学习如何在 Next.js 14/15 App Router 应用中优化 Core Web Vitals,掌握针对 LCP、INP 和 CLS 的实用技巧。

前置条件

第 1 步:使用 Next.js Image 组件优化 LCP

import Image from 'next/image';

export default function Hero() {
  return (
    <div className="hero">
      <Image
        src="/hero.jpg"
        alt="Hero image"
        width={1200}
        height={630}
        priority // Preload this image
        sizes="100vw"
        className="hero-image"
      />
    </div>
  );
}

图片优化配置

// next.config.js
module.exports = {
  images: {
    formats: ['image/avif', 'image/webp'],
    deviceSizes: [640, 750, 828, 1080, 1200, 1920],
    imageSizes: [16, 32, 48, 64, 96, 128, 256],
  },
};

第 2 步:优化字体加载

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

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-inter',
});

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

Next.js 会自动完成以下工作:

第 3 步:使用 Server Components 降低 INP

// Move heavy computations to server
// app/articles/page.js
async function ArticlesPage() {
  // This runs on the server - zero client JS
  const articles = await fetchArticles();
  const processedData = heavyComputation(articles);

  return (
    <div>
      {processedData.map(article => (
        <ArticleCard key={article.id} article={article} />
      ))}
    </div>
  );
}

Client Component 优化

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

export function SearchResults({ query }) {
  const [isPending, startTransition] = useTransition();

  const filteredResults = useMemo(() => {
    return expensiveFilter(query);
  }, [query]);

  return (
    <div style={{ opacity: isPending ? 0.7 : 1 }}>
      {filteredResults.map(result => (
        <ResultCard key={result.id} result={result} />
      ))}
    </div>
  );
}

第 4 步:通过流式 SSR 避免 CLS

// app/page.js
import { Suspense } from 'react';

export default function Page() {
  return (
    <main>
      <HeroSection /> {/* Static, renders immediately */}
      <Suspense fallback={<ProductSkeleton />}>
        <ProductList /> {/* Dynamic, streams in */}
      </Suspense>
      <Suspense fallback={<ReviewSkeleton />}>
        <Reviews /> {/* Independent suspense boundary */}
      </Suspense>
    </main>
  );
}

骨架屏组件

function ProductSkeleton() {
  return (
    <div className="grid grid-cols-3 gap-4" aria-busy="true">
      {Array.from({ length: 6 }).map((_, i) => (
        <div key={i} className="animate-pulse">
          <div className="bg-gray-200 rounded-lg h-48 w-full" />
          <div className="bg-gray-200 rounded h-4 w-3/4 mt-4" />
          <div className="bg-gray-200 rounded h-4 w-1/2 mt-2" />
        </div>
      ))}
    </div>
  );
}

第 5 步:实现路由预取

import Link from 'next/link';

// Next.js prefetches linked routes in viewport
export function Navigation() {
  return (
    <nav>
      <Link href="/products" prefetch={true}>Products</Link>
      <Link href="/about" prefetch={false}>About</Link>
    </nav>
  );
}

第 6 步:配置缓存响应头

// next.config.js
module.exports = {
  async headers() {
    return [
      {
        source: '/:path*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, max-age=3600, stale-while-revalidate=86400',
          },
        ],
      },
    ];
  },
};

第 7 步:性能监控

// app/layout.js - Report Web Vitals
export function reportWebVitals(metric) {
  fetch('/api/vitals', {
    method: 'POST',
    body: JSON.stringify(metric),
  });
}

结果汇总

指标 优化前 优化后
LCP 4.2s 1.8s
INP 350ms 120ms
CLS 0.25 0.05
打包体积 380KB 145KB
FCP 2.8s 0.9s