Next.js Static Site Generation (SSG)

Overview

Static Site Generation (SSG) is how Next.js pre-renders pages at build time, generating static HTML files for optimal performance and SEO.

What is SSG?

How SSG Works

  1. Data is fetched at build time
  2. Static HTML files are generated
  3. Files are deployed to a CDN
  4. HTML is returned directly when users visit
  5. Client-side hydration activates the page

Advantages of SSG

  • Extremely fast loading speeds
  • Excellent SEO
  • Low server costs
  • Easy CDN distribution
  • Higher security

SSG in the App Router

Default Static Generation

// app/page.tsx
// 默认情况下,没有动态函数的页面会被静态生成
export default function Page() {
  return <h1>静态页面</h1>
}

Static Generation with Data

// app/posts/page.tsx
async function getPosts() {
  const res = await fetch('https://api.example.com/posts')
  return res.json()
}

export default async function Posts() {
  const posts = await getPosts()
  
  return (
    <div>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
        </article>
      ))}
    </div>
  )
}

Dynamic Route Static Generation

// app/posts/[id]/page.tsx
export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts').then(res => res.json())
  
  return posts.map((post) => ({
    id: post.id,
  }))
}

async function getPost(id: string) {
  const res = await fetch(`https://api.example.com/posts/${id}`)
  return res.json()
}

export default async function Post({ params }: { params: { id: string } }) {
  const post = await getPost(params.id)
  
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  )
}

SSG in the Pages Router

getStaticProps

// pages/posts/index.tsx
import { GetStaticProps } from 'next'

interface Post {
  id: string
  title: string
}

interface Props {
  posts: Post[]
}

export const getStaticProps: GetStaticProps<Props> = async () => {
  const res = await fetch('https://api.example.com/posts')
  const posts = await res.json()
  
  return {
    props: {
      posts,
    },
    revalidate: 60, // ISR: 每60秒重新生成
  }
}

export default function Posts({ posts }: Props) {
  return (
    <div>
      {posts.map(post => (
        <article key={post.id}>{post.title}</article>
      ))}
    </div>
  )
}

getStaticPaths

// pages/posts/[id].tsx
import { GetStaticPaths, GetStaticProps } from 'next'

export const getStaticPaths: GetStaticPaths = async () => {
  const res = await fetch('https://api.example.com/posts')
  const posts = await res.json()
  
  const paths = posts.map((post) => ({
    params: { id: post.id },
  }))
  
  return {
    paths,
    fallback: false, // 404 for other routes
  }
}

export const getStaticProps: GetStaticProps = async ({ params }) => {
  const res = await fetch(`https://api.example.com/posts/${params!.id}`)
  const post = await res.json()
  
  return {
    props: {
      post,
    },
  }
}

export default function Post({ post }) {
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  )
}

Fallback Modes

fallback: false

export const getStaticPaths = async () => {
  return {
    paths: [
      { params: { id: '1' } },
      { params: { id: '2' } },
    ],
    fallback: false, // 其他路径返回 404
  }
}

fallback: true

// pages/posts/[id].tsx
import { useRouter } from 'next/router'

export const getStaticPaths = async () => {
  return {
    paths: [{ params: { id: '1' } }],
    fallback: true, // 其他路径会在首次访问时生成
  }
}

export default function Post({ post }) {
  const router = useRouter()
  
  if (router.isFallback) {
    return <div>加载中...</div>
  }
  
  return <article>{post.title}</article>
}

fallback: 'blocking'

export const getStaticPaths = async () => {
  return {
    paths: [],
    fallback: 'blocking', // 服务端生成,不显示加载状态
  }
}

Incremental Static Regeneration (ISR)

App Router ISR

// app/posts/page.tsx
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { revalidate: 60 } // 每60秒重新验证
  })
  return res.json()
}

export default async function Posts() {
  const posts = await getPosts()
  return <div>{/* ... */}</div>
}

Pages Router ISR

// pages/posts/index.tsx
export const getStaticProps = async () => {
  const posts = await fetchPosts()
  
  return {
    props: { posts },
    revalidate: 60, // 每60秒重新生成
  }
}

On-Demand Revalidation

// app/api/revalidate/route.ts
import { revalidatePath } from 'next/cache'
import { NextRequest } from 'next/server'

export async function POST(request: NextRequest) {
  const path = request.nextUrl.searchParams.get('path')
  
  if (path) {
    revalidatePath(path)
    return Response.json({ revalidated: true, now: Date.now() })
  }
  
  return Response.json({ revalidated: false, now: Date.now() })
}

Usage:

curl -X POST http://localhost:3000/api/revalidate?path=/posts

Performance Optimization

1. Partial Pre-Rendering

// app/posts/[id]/page.tsx
export async function generateStaticParams() {
  // 只预渲染热门文章
  const popularPosts = await getPopularPosts()
  
  return popularPosts.slice(0, 100).map((post) => ({
    id: post.id,
  }))
}

export const dynamicParams = true // 允许其他路径动态生成

2. Parallel Data Fetching

async function getPageData() {
  const [posts, categories, tags] = await Promise.all([
    fetch('https://api.example.com/posts').then(r => r.json()),
    fetch('https://api.example.com/categories').then(r => r.json()),
    fetch('https://api.example.com/tags').then(r => r.json()),
  ])
  
  return { posts, categories, tags }
}

3. Image Optimization

import Image from 'next/image'

export default function Post({ post }) {
  return (
    <article>
      <Image
        src={post.coverImage}
        alt={post.title}
        width={1200}
        height={630}
        priority // 优先加载
      />
      <h1>{post.title}</h1>
    </article>
  )
}

Practical Examples

Blog Website

// app/blog/page.tsx
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { revalidate: 3600 } // 1小时
  })
  return res.json()
}

export default async function Blog() {
  const posts = await getPosts()
  
  return (
    <div className="blog-grid">
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
          <Link href={`/blog/${post.slug}`}>阅读更多</Link>
        </article>
      ))}
    </div>
  )
}

// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts')
    .then(res => res.json())
  
  return posts.map((post) => ({
    slug: post.slug,
  }))
}

async function getPost(slug: string) {
  const res = await fetch(`https://api.example.com/posts/${slug}`, {
    next: { revalidate: 3600 }
  })
  return res.json()
}

export default async function BlogPost({ params }) {
  const post = await getPost(params.slug)
  
  return (
    <article>
      <h1>{post.title}</h1>
      <time>{post.publishedAt}</time>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  )
}

Product Catalog

// app/products/page.tsx
async function getProducts() {
  const res = await fetch('https://api.example.com/products', {
    next: { revalidate: 300 } // 5分钟
  })
  return res.json()
}

export default async function Products() {
  const products = await getProducts()
  
  return (
    <div className="product-grid">
      {products.map(product => (
        <div key={product.id}>
          <Image
            src={product.image}
            alt={product.name}
            width={300}
            height={300}
          />
          <h3>{product.name}</h3>
          <p>{product.price}元</p>
        </div>
      ))}
    </div>
  )
}

Best Practices

1. Choose the Right Rendering Strategy

// 完全静态 - 内容很少变化
export default async function AboutPage() {
  return <div>关于我们</div>
}

// ISR - 内容定期更新
async function getData() {
  const res = await fetch('url', {
    next: { revalidate: 3600 }
  })
  return res.json()
}

// 动态 - 内容频繁变化
async function getData() {
  const res = await fetch('url', {
    cache: 'no-store'
  })
  return res.json()
}

2. Optimize Build Time

// 限制预渲染数量
export async function generateStaticParams() {
  const posts = await getPosts()
  
  // 只预渲染最新的100篇文章
  return posts.slice(0, 100).map(post => ({
    id: post.id,
  }))
}

// 允许其他路径按需生成
export const dynamicParams = true

3. Error Handling

export async function generateStaticParams() {
  try {
    const posts = await getPosts()
    return posts.map(post => ({ id: post.id }))
  } catch (error) {
    console.error('获取文章列表失败:', error)
    return [] // 返回空数组,避免构建失败
  }
}

Previous Chapter: Server-Side Rendering (SSR) | Next Chapter: Incremental Static Regeneration (ISR)