学习如何在 Next.js 14/15 App Router 应用中优化 Core Web Vitals,掌握针对 LCP、INP 和 CLS 的实用技巧。
前置条件
- 使用 App Router 的 Next.js 14+ 项目
- 对 React Server Components 有基本了解
- Vercel 部署环境(或类似环境)
第 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 会自动完成以下工作:
- 内联字体 CSS
- 预加载字体文件
- 使用
font-display: swap - 消除字体加载导致的布局偏移
第 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 |