Next.js App Router效能最佳化:從SSR到流式渲染的7個關鍵調優策略
前端工程
首屏3秒變成了5秒,App Router到底怎麼了
你從Pages Router遷移到App Router,本以為RSC(React Server Components)能讓頁面更快,結果首屏反而變慢了。DevTools裡一堆瀑布流請求,LCP從2秒飆到5秒,CLS也飄紅了。App Router不是銀彈——它的流式渲染、RSC、快取機制需要你主動調優,否則預設配置可能比Pages Router還慢。
本文將從SSR最佳化出發,帶你完成流式渲染→RSC最佳化→快取策略→Bundle瘦身→Core Web Vitals→資料獲取→路由預載入的7個關鍵調優策略,讓App Router真正發揮效能優勢。
App Router效能核心概念
| 概念 | 說明 |
|---|---|
| React Server Components (RSC) | 服務端元件,零客戶端JS,直接在服務端渲染 |
| Streaming SSR | 流式服務端渲染,分塊傳輸HTML,首屏更快 |
| Suspense Boundary | Suspense邊界,控制流式渲染的分塊粒度 |
| Static/Dynamic Rendering | 靜態/動態渲染,建構時vs請求時渲染 |
| ISR (Incremental Static Regeneration) | 增量靜態再生成,定時更新靜態頁面 |
| Route Segment Config | 路由段配置,控制每個路由的渲染和快取行為 |
| Partial Prerendering (PPR) | 部分預渲染,靜態殼+動態內容的混合模式 |
| Turbopack | 新建構工具,比Webpack快700倍(開發模式) |
渲染模式對比
靜態渲染(SSG):
建構時生成HTML → CDN快取 → 使用者直接獲取 → TTFB極低
動態渲染(SSR):
請求時生成HTML → 流式傳輸 → 使用者逐步看到內容 → TTFB中等
流式渲染(Streaming SSR):
請求時開始渲染 → Suspense分塊 → 先傳送靜態部分 → 動態部分就緒後追加
部分預渲染(PPR):
建構時生成靜態殼 → 請求時填充動態內容 → 兼顧SSG速度和SSR即時性
問題分析:App Router效能的5大挑戰
- 瀑布流資料獲取:Server Components巢狀導致串列資料獲取,TTFB線性增長
- 客戶端JS膨脹:誤用"use client"導致大量元件被打包到客戶端Bundle
- 快取失效混亂:fetch快取、Router快取、Full Route Cache三層快取難以協調
- 流式渲染粒度不當:Suspense邊界太多導致多次往返,太少又失去流式優勢
- Core Web Vitals退化:RSC的HTML體積更大,CLS/LCP/FID可能比Pages Router差
分步實操:7個關鍵調優策略
策略1:流式渲染與Suspense邊界設計
// app/page.tsx
import { Suspense } from 'react';
export default function HomePage() {
return (
<div>
<HeroSection />
<Suspense fallback={<ProductGridSkeleton />}>
<ProductGrid />
</Suspense>
<Suspense fallback={<RecommendationsSkeleton />}>
<Recommendations />
</Suspense>
<Suspense fallback={<ReviewsSkeleton />}>
<Reviews />
</Suspense>
</div>
);
}
function ProductGridSkeleton() {
return (
<div className="grid grid-cols-4 gap-4">
{Array.from({ length: 8 }).map((_, i) => (
<div key={i} className="animate-pulse">
<div className="bg-gray-200 h-48 rounded-lg" />
<div className="bg-gray-200 h-4 mt-2 rounded w-3/4" />
<div className="bg-gray-200 h-4 mt-1 rounded w-1/2" />
</div>
))}
</div>
);
}
// app/components/ProductGrid.tsx
async function ProductGrid() {
const products = await fetchProducts();
return (
<div className="grid grid-cols-4 gap-4">
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
策略2:RSC資料獲取最佳化——並行請求
// ❌ 串列獲取:總耗時 = fetchA + fetchB + fetchC
async function Page() {
const dataA = await fetchA();
const dataB = await fetchB(dataA.id);
const dataC = await fetchC();
return <Dashboard a={dataA} b={dataB} c={dataC} />;
}
// ✅ 並行獲取:總耗時 = max(fetchA, fetchB, fetchC)
async function Page() {
const [dataA, dataB, dataC] = await Promise.all([
fetchA(),
fetchB(),
fetchC(),
]);
return <Dashboard a={dataA} b={dataB} c={dataC} />;
}
// ✅ 更優:Suspense邊界分離,獨立流式
async function Page() {
return (
<div>
<Suspense fallback={<SkeletonA />}>
<ComponentA />
</Suspense>
<Suspense fallback={<SkeletonB />}>
<ComponentB />
</Suspense>
<Suspense fallback={<SkeletonC />}>
<ComponentC />
</Suspense>
</div>
);
}
async function ComponentA() {
const dataA = await fetchA();
return <DashboardA data={dataA} />;
}
async function ComponentB() {
const dataB = await fetchB();
return <DashboardB data={dataB} />;
}
async function ComponentC() {
const dataC = await fetchC();
return <DashboardC data={dataC} />;
}
策略3:快取策略三層協調
// 1. fetch級別快取
async function getProducts() {
const res = await fetch('https://api.example.com/products', {
next: {
revalidate: 3600, // ISR: 每小時重新驗證
tags: ['products'], // 按需失效
},
});
return res.json();
}
// 2. 路由段配置
// app/products/page.tsx
export const revalidate = 3600; // 頁面級ISR
export const dynamic = 'force-static'; // 強制靜態渲染
export const dynamicParams = true; // 允許動態引數
// 3. 按需快取失效
// app/api/revalidate/route.ts
import { revalidateTag, revalidatePath } from 'next/cache';
import { NextRequest } from 'next/server';
export async function POST(request: NextRequest) {
const body = await request.json();
if (body.tag) {
revalidateTag(body.tag);
}
if (body.path) {
revalidatePath(body.path);
}
return Response.json({ revalidated: true, now: Date.now() });
}
策略4:客戶端Bundle瘦身
// ❌ 整個lodash被打包到客戶端
'use client';
import { debounce } from 'lodash';
export function SearchInput() {
const handleChange = debounce((e) => { ... }, 300);
return <input onChange={handleChange} />;
}
// ✅ 使用輕量替代或僅匯入需要的函式
'use client';
import { useDeferredValue, useState } from 'react';
export function SearchInput() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
return (
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
);
}
// ✅ 更優:重型互動留在Server Component,客戶端只做最小互動
async function ProductList({ query }: { query: string }) {
const products = await searchProducts(query);
return (
<div>
{products.map((p) => (
<ProductCard key={p.id} product={p} />
))}
</div>
);
}
// 最小客戶端元件
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
export function SearchForm() {
const router = useRouter();
const searchParams = useSearchParams();
return (
<form>
<input
defaultValue={searchParams.get('q') || ''}
onChange={(e) => {
router.push(`/products?q=${e.target.value}`);
}}
/>
</form>
);
}
策略5:Core Web Vitals最佳化
// LCP最佳化:預載入關鍵資源
// app/layout.tsx
import type { Metadata } from 'next';
export const metadata: Metadata = {
other: {
'preconnect': 'https://api.example.com',
},
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh-TW">
<head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
</head>
<body>
{children}
</body>
</html>
);
}
// CLS最佳化:圖片佔位
import Image from 'next/image';
function HeroImage() {
return (
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={600}
priority
placeholder="blur"
blurDataURL="/hero-blur.jpg"
sizes="(max-width: 768px) 100vw, 1200px"
/>
);
}
// INP最佳化:減少主執行緒阻塞
'use client';
import { useTransition } from 'react';
export function FilterButton({ filter, onFilter }: { filter: string; onFilter: (f: string) => void }) {
const [isPending, startTransition] = useTransition();
return (
<button
onClick={() => startTransition(() => onFilter(filter))}
className={isPending ? 'opacity-50' : ''}
>
{filter}
</button>
);
}
策略6:資料獲取模式最佳化
// app/products/[id]/page.tsx
import { notFound } from 'next/navigation';
export async function generateStaticParams() {
const products = await fetchTopProducts();
return products.map((p) => ({ id: p.id }));
}
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await getProduct(params.id);
if (!product) {
notFound();
}
return (
<div>
<h1>{product.name}</h1>
<Suspense fallback={<RelatedSkeleton />}>
<RelatedProducts productId={product.id} />
</Suspense>
</div>
);
}
async function getProduct(id: string) {
const res = await fetch(`https://api.example.com/products/${id}`, {
next: { revalidate: 600, tags: [`product-${id}`] },
});
if (!res.ok) return null;
return res.json();
}
async function fetchTopProducts() {
const res = await fetch('https://api.example.com/products/top', {
next: { revalidate: 3600, tags: ['products'] },
});
return res.json();
}
async function RelatedProducts({ productId }: { productId: string }) {
const products = await fetch(`https://api.example.com/products/${productId}/related`, {
next: { revalidate: 300 },
}).then(r => r.json());
return (
<div className="grid grid-cols-4 gap-4">
{products.map((p: any) => (
<ProductCard key={p.id} product={p} />
))}
</div>
);
}
策略7:路由預載入與預取
// app/components/Navigation.tsx
import Link from 'next/link';
export function Navigation() {
return (
<nav>
<Link href="/products" prefetch={true}>
產品
</Link>
<Link href="/about" prefetch={false}>
關於
</Link>
<Link href="/contact">
聯絡
</Link>
</nav>
);
}
// 視口內預載入
'use client';
import { useRouter } from 'next/navigation';
export function usePrefetchOnHover(href: string) {
const router = useRouter();
const handleMouseEnter = () => {
router.prefetch(href);
};
return { onMouseEnter: handleMouseEnter };
}
function SmartLink({ href, children }: { href: string; children: React.ReactNode }) {
const { onMouseEnter } = usePrefetchOnHover(href);
return (
<Link href={href} onMouseEnter={onMouseEnter}>
{children}
</Link>
);
}
避坑指南
坑1:Server Component中使用了客戶端API
// ❌ 錯誤:Server Component中使用useState
async function Counter() {
const [count, setCount] = useState(0); // 編譯報錯!
return <div>{count}</div>;
}
// ✅ 正確:拆分為Server + Client元件
async function CounterWrapper() {
const initialValue = await fetchCount();
return <CounterClient initial={initialValue} />;
}
'use client';
function CounterClient({ initial }: { initial: number }) {
const [count, setCount] = useState(initial);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
坑2:fetch快取預設行為變化
// ❌ 錯誤:App Router中fetch預設快取(不同於Pages Router)
async function getData() {
const res = await fetch('https://api.example.com/data');
// 預設被快取!資料不會即時更新
return res.json();
}
// ✅ 正確:顯式宣告快取策略
async function getData() {
const res = await fetch('https://api.example.com/data', {
cache: 'no-store', // 每次請求都獲取最新資料
});
return res.json();
}
// ✅ 或者使用ISR
async function getData() {
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 60 }, // 60秒後重新驗證
});
return res.json();
}
坑3:Suspense邊界過多導致水合延遲
// ❌ 錯誤:每個小元件都包Suspense
<Suspense fallback={<S />}><UserName /></Suspense>
<Suspense fallback={<S />}><UserEmail /></Suspense>
<Suspense fallback={<S />}><UserAvatar /></Suspense>
// 3次網路往返!
// ✅ 正確:相關元件共享一個Suspense邊界
<Suspense fallback={<UserProfileSkeleton />}>
<UserProfile /> {/* 內部包含UserName, UserEmail, UserAvatar */}
</Suspense>
坑4:"use client"濫用導致Bundle膨脹
// ❌ 錯誤:整個頁面標記為客戶端元件
'use client';
export default function ProductsPage() {
// 整個頁面及其所有子元件都進入客戶端Bundle
return <ProductList />;
}
// ✅ 正確:只有互動部分標記為客戶端
export default async function ProductsPage() {
const products = await fetchProducts(); // Server端獲取
return (
<div>
<h1>產品列表</h1> {/* Server Component */}
<ProductGrid products={products} /> {/* Server Component */}
<AddToCartButton /> {/* 'use client' —— 只有這個需要客戶端 */}
</div>
);
}
坑5:動態路由未配置generateStaticParams
// ❌ 錯誤:動態路由沒有generateStaticParams,每次請求都SSR
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await getProduct(params.id);
return <div>{product.name}</div>;
}
// ✅ 正確:新增generateStaticParams實現SSG
export async function generateStaticParams() {
const products = await fetchAllProducts();
return products.map((p) => ({ id: p.id }));
}
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await getProduct(params.id);
return <div>{product.name}</div>;
}
報錯排查
| 序號 | 報錯訊息 | 原因 | 解決方法 |
|---|---|---|---|
| 1 | Functions cannot be passed directly to Client Components |
Server Component傳遞函式給Client Component | 將函式邏輯移到Client Component內部 |
| 2 | Route "/xxx" used \cookies()` but is statically generated` |
靜態頁面中呼叫了動態API | 新增export const dynamic = 'force-dynamic' |
| 3 | Hydration mismatch |
服務端和客戶端渲染內容不一致 | 檢查Date.now()、random等,使用suppressHydrationWarning |
| 4 | Maximum call stack size exceeded |
迴圈依賴或無限重渲染 | 檢查useEffect依賴,避免迴圈state更新 |
| 5 | fetch failed with status 500 |
API請求失敗且被快取 | 使用cache: 'no-store'或新增錯誤處理 |
| 6 | Module not found: Can't resolve 'xxx' |
客戶端元件引用了Node.js模組 | 將Node.js邏輯移到Server Component或Route Handler |
| 7 | useSearchParams() should be wrapped in Suspense |
useSearchParams未包裹Suspense | 在外層新增Suspense boundary |
| 8 | Static generation timeout |
generateStaticParams執行超時 | 最佳化資料獲取,增加staticGenerationTimeout配置 |
| 9 | The "use cache" directive requires experimental flag |
使用了實驗性快取API | 在next.config.js中啟用experimental.useCache |
| 10 | Turbopack build error |
Turbopack不相容某些Webpack loader | 開發用Turbopack,生產用Webpack或升級依賴 |
進階最佳化
1. Partial Prerendering(PPR)
// app/page.tsx — 啟用PPR(Next.js 15 experimental)
export const experimental_ppr = true;
export default function HomePage() {
return (
<div>
{/* 靜態殼 —— 建構時渲染,CDN快取 */}
<Header />
<HeroBanner />
{/* 動態內容 —— 請求時渲染,流式傳輸 */}
<Suspense fallback={<PersonalizedFeedSkeleton />}>
<PersonalizedFeed />
</Suspense>
{/* 靜態內容 —— 建構時渲染 */}
<Footer />
</div>
);
}
async function PersonalizedFeed() {
const user = await getCurrentUser();
const feed = await fetchPersonalizedFeed(user.id);
return <Feed items={feed} />;
}
2. React Cache與單次獲取保證
import { cache } from 'react';
export const getProduct = cache(async (id: string) => {
const res = await fetch(`https://api.example.com/products/${id}`, {
next: { revalidate: 600 },
});
return res.json();
});
// 多個元件呼叫getProduct(id),只發一次請求
async function ProductHeader({ id }: { id: string }) {
const product = await getProduct(id);
return <h1>{product.name}</h1>;
}
async function ProductPrice({ id }: { id: string }) {
const product = await getProduct(id); // 命中快取,不再請求
return <span>{product.price}</span>;
}
3. Script載入策略最佳化
// app/layout.tsx
import Script from 'next/script';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh-TW">
<body>
{children}
<Script
src="https://analytics.example.com/script.js"
strategy="lazyOnload"
/>
<Script
src="https://cdn.example.com/chat-widget.js"
strategy="afterInteractive"
/>
</body>
</html>
);
}
對比分析
| 維度 | App Router | Pages Router | Remix | Nuxt 3 | SvelteKit |
|---|---|---|---|---|---|
| RSC支援 | ✅原生 | ❌ | ❌ | ❌ | ❌ |
| 流式渲染 | ✅原生 | ❌ | ✅ | ✅ | ✅ |
| 靜態生成 | ✅SSG/ISR | ✅SSG/ISR | ⚠️有限 | ✅ | ✅ |
| 快取層級 | ✅3層 | ⚠️1層 | ⚠️1層 | ⚠️2層 | ⚠️2層 |
| Bundle最佳化 | ✅RSC零JS | ⚠️全量 | ⚠️ | ✅ | ✅編譯最佳化 |
| 學習曲線 | ⭐陡 | ⭐平緩 | ⭐中 | ⭐中 | ⭐中 |
| 生態成熟度 | ⭐高 | ⭐極高 | ⭐中 | ⭐高 | ⭐中 |
| 開發體驗 | ⭐Turbopack | ⭐Webpack | ⭐Vite | ⭐Vite | ⭐Vite |
總結:App Router的效能不是自動獲得的——它給了你更精細的控制權,但也要求你更深入地理解渲染模型。2026年的調優路徑:先用Suspense邊界實現流式渲染(首屏提升50%+)→再清理"use client"濫用(Bundle減少30%+)→然後協調三層快取策略→最後用PPR實現靜態殼+動態內容的極致組合。關鍵原則:能Server就不Client,能並行就不串列,能快取就不請求。
線上工具推薦
- JSON格式化:/zh-TW/json/format
- Base64編解碼:/zh-TW/encode/base64
- Hash計算:/zh-TW/encode/hash
- JWT解碼:/zh-TW/encode/jwt-decode
本站提供瀏覽器本地工具,免註冊即可試用 →
#Next.js#App Router#性能优化#SSR#流式渲染#2026#React