開始使用 Next.js 15

開始使用 Next.js 15
Ian Chou
Ian Chou

學習如何使用 Next.js 15 建立現代化的 React 應用程式,包含 App Router、Server Components 等最新功能。

Next.jsReactTypeScript

Next.js 15 與 14 版本的主要差異

Next.js 15 在 14 版本的基礎上進行了多項重要改進和功能強化。以下是主要差異:

功能Next.js 14Next.js 15
Partial Prerendering (PPR)實驗階段穩定版
TurbopackBeta預設打包工具
React 版本React 18React 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 生態圈。主要優勢在於:

  1. Partial Prerendering:靜態與動態內容的完美結合,從根本上改善使用者體驗
  2. Turbopack 穩定版:更快的開發流程,更高效的構建過程
  3. React 19 整合:最新 React 特性的完美支持
  4. Server Actions 增強:更安全、更直覺的伺服器端操作
  5. 更現代的工具鏈:從 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.

More Articles
Published May 10, 202517 min read3 tags