開始使用 Next.js 15


學習如何使用 Next.js 15 建立現代化的 React 應用程式,包含 App Router、Server Components 等最新功能。
Next.js 15 與 14 版本的主要差異
Next.js 15 在 14 版本的基礎上進行了多項重要改進和功能強化。以下是主要差異:
功能 | Next.js 14 | Next.js 15 |
---|---|---|
Partial Prerendering (PPR) | 實驗階段 | 穩定版 |
Turbopack | Beta | 預設打包工具 |
React 版本 | React 18 | React 19 |
Server Actions | 實驗功能 | 正式版 |
Edge Runtime | 基礎支援 | 強化能力 |
MDX 支援 | 需額外配置 | 內建支援 |
Metadata API | 基礎功能 | 改進的 API |
Next.js 15 的重大優勢與新特性
重要更新
Next.js 15 已經正式發布,帶來了多項重要改進和功能強化。它是著眼於效能和開發體驗的重要版本。
Next.js 15 在原有 App Router、Server Components、Edge Rendering 等基礎上,進一步強化了開發效率、效能與 DX(Developer Experience)。主要亮點如下:
1. 穩定的 Partial Prerendering(PPR)
PPR 是 Next.js 15 最重要的突破,它允許頁面同時結合靜態與動態內容,從根本上改變了 Web 應用的渲染方式:
- 靜態外殼優先載入:頁面的靜態部分(如頁面框架、導航等)即刻呈現
- 動態區塊平行處理:使用者介面中的動態區域(如個人化內容)通過 Suspense 邊界平行載入
- 增量靜態再生(ISR)與 PPR 結合:靜態內容可設定重新驗證時間
與 Next.js 14 的實驗性 PPR 相比,15 版本的 PPR 提供:
- 更穩定的行為和可預測性
- 更完善的開發者工具支援
- 與 React 19 新特性的深度整合
// app/dashboard/page.tsx - Next.js 15 的 PPR 範例
import { Suspense } from 'react';
import StaticAnalytics from './StaticAnalytics';
import DynamicUserContent from './DynamicUserContent';
// 頁面結合靜態與動態內容
export default function Dashboard() {
return (
<main>
{/* 靜態區域:立即渲染 */}
<StaticAnalytics />
{/* 動態區域:平行處理,不阻塞靜態內容 */}
<Suspense fallback={<p>載入個人化內容中...</p>}>
<DynamicUserContent />
</Suspense>
</main>
);
}
// 設定靜態外殼的重新驗證時間
export const revalidate = 3600; // 1小時
2. Turbopack 穩定版
Turbopack 從 Beta 階段進入穩定版,取代 Webpack 成為 Next.js 15 的預設打包工具:
- 開發啟動速度提升 5-10 倍:相比 Next.js 14,開發伺服器啟動速度大幅提升
- 熱更新速度提升 3-5 倍:代碼修改反饋更快,開發流程更順暢
- 記憶體使用效率提升:大型專案記憶體占用減少約 40%
- 完整功能支援:支援所有 Next.js 特性,包括自定義 webpack 配置的平滑轉換
在 Next.js 14 中,Turbopack 還有相容性問題,但在 15 版本中已經全部解決。
# Next.js 15 中默認啟用 Turbopack
next dev
# 如需特別指定
next dev --turbo
3. React 19 支援
Next.js 15 是首個原生整合 React 19 的主流框架,帶來多項突破性能力:
- 更強大的 Suspense:針對數據獲取、代碼分割、圖片載入的進階支援
- Actions 與 Server Functions:與 Server Actions 深度整合
- Document Metadata API:原生支援 title、meta 等設定,SEO 更簡單
- 並發模式與優先級調度:UI 渲染可中斷,優先處理高優先級更新
相比 Next.js 14 使用的 React 18,這些改進使得複雜應用的性能和用戶體驗有了質的提升。
4. Server Actions 正式版
Server Actions 在 Next.js 15 正式穩定,功能更強大,安全性更高:
// app/actions.ts - Server Actions 在 Next.js 15 中的寫法
'use server';
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
import { revalidatePath } from 'next/cache';
// 強化類型安全
type FormState = {
error?: string;
success?: boolean;
};
// 改進的錯誤處理和表單驗證
export async function handleFormSubmission(
prevState: FormState,
formData: FormData
): Promise<FormState> {
try {
const email = formData.get('email') as string;
if (!email || !email.includes('@')) {
return { error: '請提供有效的電子郵件' };
}
// 處理表單提交
await saveToDatabase(email);
// 設置 cookies
cookies().set('subscribed', 'true');
// 重新驗證頁面數據
revalidatePath('/newsletter');
return { success: true };
} catch (error) {
return { error: '提交失敗,請稍後再試' };
}
}
相比 Next.js 14 的 Server Actions:
- 更完善的錯誤處理機制
- 更強的類型安全性
- 與 React 19 的 useOptimistic 更好整合
- 改進的表單驗證體驗
5. Edge Runtime 強化
Next.js 15 全面擴展了 Edge Runtime 的能力:
- API Routes 全面支援 Edge:比 Node.js 環境更快的冷啟動速度
- Middleware 功能增強:更強大的請求處理能力,例如 A/B 測試、地理定位等
- Edge 上的 Server Actions:動態功能無需完整 Node.js 運行時
// app/api/geo/route.ts - Edge API Route 範例
import { NextRequest } from 'next/server';
export const runtime = 'edge';
export async function GET(request: NextRequest) {
const { geo } = request;
// 根據地理位置提供本地化內容
return Response.json({
country: geo?.country || 'unknown',
city: geo?.city || 'unknown',
region: geo?.region || 'unknown',
});
}
Next.js 14 雖然支援 Edge Runtime,但 15 版本在穩定性和功能廣度上有顯著提升。
6. MDX 2.0 原生支援
Next.js 15 內建 MDX 2.0 支援,不再需要額外配置:
- 自動編譯與優化:MDX 文件自動處理,靜態提取元數據
- React Components 與 Markdown 無縫融合:更豐富的內容展示
- Metadata API 整合:從 MDX 前置元數據自動生成頁面 metadata
// app/blog/[slug]/page.tsx - MDX 整合範例
import { getMDXBySlug, getAllSlugs } from '@/lib/mdx';
// 靜態生成所有文章路徑
export async function generateStaticParams() {
const slugs = await getAllSlugs();
return slugs.map(slug => ({ slug }));
}
// 自動從 MDX 生成頁面元數據
export async function generateMetadata({ params }) {
const { slug } = params;
const { meta } = await getMDXBySlug(slug);
return {
title: meta.title,
description: meta.description,
openGraph: {
images: [meta.image],
},
};
}
// 渲染 MDX 內容
export default async function Post({ params }) {
const { slug } = params;
const { content, meta } = await getMDXBySlug(slug);
return (
<article>
<h1>{meta.title}</h1>
<time>{meta.date}</time>
{content}
</article>
);
}
相較於 Next.js 14 需要手動配置 MDX 支援,15 版本提供了更無縫的體驗。
7. 更簡潔的 Metadata API
Next.js 15 改進了 Metadata API,使 SEO 和社交分享設定更簡單:
// app/layout.tsx - 改進的 Metadata API
Next.js 15 與 14 相比,增加了更多內置的 metadata 類型,提升了 SEO 優化體驗。
開始使用 Next.js 15
安裝
用最新版 create-next-app 快速建立專案:
npx create-next-app@latest my-app
系統會提示您進行初始配置:
✔ Would you like to use TypeScript? ... Yes
✔ Would you like to use ESLint? ... Yes
✔ Would you like to use Tailwind CSS? ... Yes
✔ Would you like to use `src/` directory? ... No
✔ Would you like to use App Router? (recommended) ... Yes
✔ Would you like to customize the default import alias (@/*)? ... Yes
專案結構
Next.js 15 鼓勵使用 App Router 結構:
my-app/
├── app/ # App Router 主目錄
│ ├── layout.tsx # 根佈局
│ ├── page.tsx # 首頁
│ ├── about/ # 關於頁面路由
│ │ └── page.tsx
│ ├── blog/ # 博客路由
│ │ ├── page.tsx # 博客列表頁
│ │ └── [slug]/ # 動態博客文章路由
│ │ └── page.tsx
│ └── globals.css # 全局樣式
├── components/ # 共享組件
├── lib/ # 工具函數和資料處理
├── public/ # 靜態資源
├── next.config.js # Next.js 配置
├── package.json # 依賴配置
└── tailwind.config.js # Tailwind 配置
Next.js 15 核心配置選項
相比 Next.js 14,15 版本的配置選項有所增加:
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
// 1. 新增:啟用 PPR (預設為 true,可以明確設置)
experimental: {
ppr: true,
},
// 2. 新增:指定預設 runtime
serverComponentsExternalPackages: ['sharp', 'canvas'],
// 3. 改進:圖像優化配置
images: {
formats: ['image/avif', 'image/webp'],
remotePatterns: [
{
protocol: 'https',
hostname: 'images.example.com',
},
],
minimumCacheTTL: 60,
},
// 4. 新增:Turbopack 自定義設定
turbo: {
resolveAlias: {
'custom-lib': 'custom-lib/dist',
},
},
// 5. 改進:更多細粒度控制
logging: {
fetches: {
fullUrl: true,
},
},
};
module.exports = nextConfig;
Next.js 15 最佳實踐
1. 利用 PPR 優化載入體驗
區分靜態與動態內容,利用 PPR 提升性能:
// app/products/page.tsx
import { Suspense } from 'react';
import StaticProductList from './StaticProductList';
import DynamicFilters from './DynamicFilters';
import UserRecommendations from './UserRecommendations';
export default function ProductsPage() {
return (
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
{/* 側邊欄:動態內容 */}
<div>
<Suspense fallback={<FiltersSkeleton />}>
<DynamicFilters />
</Suspense>
</div>
{/* 主內容:結合靜態與動態 */}
<div className="md:col-span-3">
{/* 靜態產品列表 */}
<StaticProductList />
{/* 動態個人推薦 */}
<section className="mt-8">
<h2>為您推薦</h2>
<Suspense fallback={<RecommendationsSkeleton />}>
<UserRecommendations />
</Suspense>
</section>
</div>
</div>
);
}
2. Server Components 與 Client Components 明確分離
// 🟢 Server Component (app/dashboard/analytics.tsx)
import { getAnalyticsData } from '@/lib/analytics';
export default async function Analytics() {
// 直接在服務器端獲取數據
const data = await getAnalyticsData();
return (
<div className="grid grid-cols-3 gap-4">
{data.metrics.map(metric => (
<div key={metric.id} className="p-4 bg-white rounded shadow">
<h3>{metric.name}</h3>
<p className="text-2xl font-bold">{metric.value}</p>
</div>
))}
</div>
);
}
// 🟠 Client Component (components/interactive/chart.tsx)
'use client';
import { useState } from 'react';
import { LineChart } from '@/components/ui/charts';
export default function InteractiveChart({ initialData }) {
const [timeRange, setTimeRange] = useState('week');
return (
<div>
<div className="flex gap-2 mb-4">
<button
onClick={() => setTimeRange('day')}
className={`px-3 py-1 rounded ${timeRange === 'day' ? 'bg-blue-500 text-white' : 'bg-gray-200'}`}
>
日
</button>
<button
onClick={() => setTimeRange('week')}
className={`px-3 py-1 rounded ${timeRange === 'week' ? 'bg-blue-500 text-white' : 'bg-gray-200'}`}
>
週
</button>
<button
onClick={() => setTimeRange('month')}
className={`px-3 py-1 rounded ${timeRange === 'month' ? 'bg-blue-500 text-white' : 'bg-gray-200'}`}
>
月
</button>
</div>
<LineChart data={initialData[timeRange]} />
</div>
);
}
3. 使用 Server Actions 處理表單
// app/contact/actions.ts
'use server';
import { z } from 'zod';
import { redirect } from 'next/navigation';
import { revalidatePath } from 'next/cache';
// 表單驗證模式
const ContactFormSchema = z.object({
name: z.string().min(2, '名稱至少需要 2 個字符'),
email: z.string().email('請提供有效的電子郵件'),
message: z.string().min(10, '訊息至少需要 10 個字符'),
});
export async function submitContactForm(prevState, formData) {
// 1. 驗證表單數據
const validatedFields = ContactFormSchema.safeParse({
name: formData.get('name'),
email: formData.get('email'),
message: formData.get('message'),
});
// 2. 如果驗證失敗,返回錯誤
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
message: '請檢查表單錯誤',
};
}
// 3. 準備要保存的數據
const { name, email, message } = validatedFields.data;
try {
// 4. 保存到數據庫
await saveMessageToDatabase({ name, email, message });
// 5. 重新驗證路徑
revalidatePath('/contact');
// 6. 返回成功
return {
message: '訊息已成功送出!',
};
} catch (error) {
// 7. 處理錯誤
return {
message: '發送訊息時出錯,請稍後再試',
};
}
}
// app/contact/page.tsx
'use client';
import { useFormState } from 'react-dom';
import { submitContactForm } from './actions';
const initialState = {
errors: {},
message: '',
};
export default function ContactPage() {
const [state, formAction] = useFormState(submitContactForm, initialState);
return (
<div className="max-w-md mx-auto p-6 bg-white rounded shadow">
<h1 className="text-2xl font-bold mb-6">聯絡我們</h1>
{state.message && (
<div className={`p-3 mb-4 rounded ${state.errors ? 'bg-red-100 text-red-800' : 'bg-green-100 text-green-800'}`}>
{state.message}
</div>
)}
<form action={formAction}>
<div className="mb-4">
<label htmlFor="name" className="block text-sm font-medium mb-1">
姓名
</label>
<input
id="name"
name="name"
type="text"
className="w-full px-3 py-2 border rounded"
/>
{state.errors?.name && (
<p className="mt-1 text-sm text-red-600">{state.errors.name[0]}</p>
)}
</div>
<div className="mb-4">
<label htmlFor="email" className="block text-sm font-medium mb-1">
電子郵件
</label>
<input
id="email"
name="email"
type="email"
className="w-full px-3 py-2 border rounded"
/>
{state.errors?.email && (
<p className="mt-1 text-sm text-red-600">{state.errors.email[0]}</p>
)}
</div>
<div className="mb-4">
<label htmlFor="message" className="block text-sm font-medium mb-1">
訊息
</label>
<textarea
id="message"
name="message"
rows={4}
className="w-full px-3 py-2 border rounded"
/>
{state.errors?.message && (
<p className="mt-1 text-sm text-red-600">{state.errors.message[0]}</p>
)}
</div>
<button
type="submit"
className="w-full bg-blue-600 text-white py-2 px-4 rounded hover:bg-blue-700"
>
送出
</button>
</form>
</div>
);
}
4. 利用 Image 組件優化圖片載入
// 比 Next.js 14 更強大的圖片優化
import Image from 'next/image';
export default function ProductGallery({ products }) {
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{products.map(product => (
<div key={product.id} className="group relative overflow-hidden rounded">
{/* 新增:支援 AVIF 格式和更多屬性 */}
<Image
src={product.image}
alt={product.name}
width={400}
height={300}
className="object-cover transition-transform group-hover:scale-105"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
priority={product.featured}
quality={85}
placeholder="blur"
blurDataURL={product.blurDataUrl}
/>
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/80 to-transparent p-4">
<h3 className="text-white font-semibold">{product.name}</h3>
<p className="text-white/80 text-sm">{product.price}</p>
</div>
</div>
))}
</div>
);
}
從 Next.js 14 遷移到 15
如果您有現有的 Next.js 14 專案想要升級到 15 版本,請遵循以下步驟:
1. 更新依賴
npm install next@latest react@latest react-dom@latest
2. 調整配置
檢查 next.config.js
中的實驗性功能:
const nextConfig = {
// 移除過時的實驗性功能
experimental: {
// appDir 已完全支持,不再需要
// appDir: true,
// serverActions 已成為正式功能,不再需要
// serverActions: true,
// 如需關閉 PPR (不推薦)
// ppr: false,
},
};
3. 利用 PPR
重構您的頁面以利用 PPR 功能,識別靜態和動態內容:
// app/profile/page.tsx
import { Suspense } from 'react';
import ProfileHeader from './ProfileHeader'; // 靜態部分
import UserStats from './UserStats'; // 動態部分
import RecentActivity from './RecentActivity'; // 動態部分
export default function ProfilePage() {
return (
<div>
{/* 靜態部分: 立即渲染 */}
<ProfileHeader />
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mt-8">
{/* 動態部分: 異步加載 */}
<Suspense fallback={<div className="h-64 bg-gray-100 animate-pulse rounded"></div>}>
<UserStats />
</Suspense>
<Suspense fallback={<div className="h-64 bg-gray-100 animate-pulse rounded"></div>}>
<RecentActivity />
</Suspense>
</div>
</div>
);
}
4. 更新 Server Actions
將 Server Actions 遷移到新的寫法,利用 React 19 的新特性:
// 舊的寫法 (Next.js 14)
'use server';
export async function updateProfile(formData) {
const name = formData.get('name');
const bio = formData.get('bio');
await db.user.update({
where: { id: userId },
data: { name, bio },
});
revalidatePath('/profile');
}
// 新的寫法 (Next.js 15)
'use server';
import { z } from 'zod';
const UpdateProfileSchema = z.object({
name: z.string().min(2),
bio: z.string().max(160),
});
export async function updateProfile(prevState, formData) {
const rawData = {
name: formData.get('name'),
bio: formData.get('bio'),
};
const validation = UpdateProfileSchema.safeParse(rawData);
if (!validation.success) {
return {
status: 'error',
errors: validation.error.flatten().fieldErrors,
};
}
const { name, bio } = validation.data;
try {
await db.user.update({
where: { id: userId },
data: { name, bio },
});
revalidatePath('/profile');
return {
status: 'success',
message: '個人資料已更新',
};
} catch (error) {
return {
status: 'error',
message: '更新失敗',
};
}
}
總結
Next.js 15 以更快的開發體驗、更彈性的渲染模式與更強的全端能力,持續領先 React 生態圈。主要優勢在於:
- Partial Prerendering:靜態與動態內容的完美結合,從根本上改善使用者體驗
- Turbopack 穩定版:更快的開發流程,更高效的構建過程
- React 19 整合:最新 React 特性的完美支持
- Server Actions 增強:更安全、更直覺的伺服器端操作
- 更現代的工具鏈:從 MDX 支援到 Metadata API,都更現代化、更易用
無論你是要打造 SaaS 產品、企業網站、電商平台還是內容導向的網站,Next.js 15 都能讓開發過程更流暢,使用者體驗更佳。
相關資源:
Interactive Components
This post includes custom interactive components for enhanced experience
Thanks for reading!
Found this article helpful? Share it with others or explore more content.