Blog

/

Frontend

Next.js - 三層渲染架構

CSRSSRSSGISR

Posted at

2024年10月13日

Posted on

Frontend

上次分享了現代框架 React 和 Vue 的一些區別,也有另外提到渲染的不同。現代框架的渲染可以分為 CSR、SSR、SSG、ISR 四種方案:

方案

載入時間

伺服器成本

SEO

內容更新速度

適用場景

主要缺點

CSR

2 - 5 秒

即時

企業內部系統、高互動工具

載入慢、SEO 無效

SSR

0.3 - 1 秒

即時

內容實時變化、登入狀態頁面

伺服器壓力大、無法用 CDN

SSG

0.05 - 0.2 秒

最低

需要重新構建

部落格、Landing Page

內容更新慢、大量頁面構建時間長

ISR

0.05 - 0.2 秒

中等

1 小時內自動更新

電商、內容平台、社交平台

初次訪問新頁面可能較慢

CSR: 純客戶端 瀏覽器包辦一切

CSR 就是傳統 React SPA。Next.js 用 getStaticProps 和 getServerSideProps 的時代已經過去ㄌ,現在在 App Router 裡做 CSR 其實有點奇怪。但有人就是這樣做,所以我們還是看看。

// app/products/page.tsx (Client Component)
'use client'

import { useEffect, useState } from 'react'

export default function Products() {
  const [products, setProducts] = useState([])
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    fetch('/api/products')
      .then(res => res.json())
      .then(data => {
        setProducts(data)
        setLoading(false)
      })
  }, [])

  if (loading) return <div>Loading...</div>
  
  return (
    <div>
      {products.map(p => (
        <div key={p.id}>{p.name}</div>
      ))}
    </div>
  )
}

流程是這樣的。用戶訪問 products 頁面,伺服器返回一個空的 HTML 加載頁面。瀏覽器下載 JavaScript,執行 React,看到 useEffect,發起 fetch 請求。伺服器返回數據,React 重新渲染,頁面才顯示出來。

整個過程下來,用戶得等 1 到 3 秒才能看到內容。而且搜索引擎爬蟲爬到的就是那個空頁面。為什麼還要用 Next.js?這時候用 Vite 加 Firebase 更輕。

CSR 唯一的優勢是搭建快,不用想伺服器邏輯。但在 Next.js 環境下,這個優勢不明顯。

SSR: 每次都在伺服器運行

SSR 是 Next.js 的經典用法。用戶訪問頁面時,伺服器執行你的 React 代碼,拿到數據,直接渲染成 HTML 返回。瀏覽器收到完整的 HTML,馬上展示。

// app/products/page.tsx (Server Component)
async function getProducts() {
  const res = await fetch('https://abby.pretty.com/products', {
    next: { revalidate: 60 } 
  })
  return res.json()
}

export default async function Products() {
  const products = await getProducts()

  return (
    <div>
      {products.map(p => (
        <div key={p.id}>{p.name}</div>
      ))}
    </div>
  )
}

這樣每次訪問都要在伺服器跑一遍 React,調用資料庫,再返回 HTML。用戶多的時候,伺服器就會卡死。而且由於每個用戶看到的內容可能不同(比如登入狀態),你沒辦法用 CDN 緩存,必須跑在伺服器上。

SSR 適合內容實時變化、登入狀態會影響顯示的場景。比如用戶的個人儀表盤、即時通知。但如果大部分用戶看到的內容都一樣,SSR 就太浪費了。

SSG:構建時一次性生成

SSG 是靜態優先的方案。你在構建時運行一次代碼,生成所有頁面的 HTML,然後部署到 CDN。用戶訪問時拿到的就是預先生成的 HTML,速度快到不行

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

export default async function Post({ params }: { params: { slug: string } }) {
  const post = await fetch(`https://abby.pretty.com/posts/${params.slug}`).then(r => r.json())

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  )
}

buildTime 時,Next.js 調用 generateStaticParams 拿到所有的 slug,然後為每個 slug 執行一遍組件代碼,生成對應的 HTML。部署後,用戶訪問就拿到預先生成的頁面,從 CDN edge 返回,延遲幾毫秒。

SSG 的問題是內容更新需要重新構建。你改了一篇文章,整個網站得重新構建才能更新。小改動也要推送,這對內容頻繁變化的網站不友好。而且如果你有幾百萬個頁面,構建時間會長到不能接受。

ISR:靜態加動態更新

ISR 是 Next.js 的創新。結合了 SSG 的速度和 SSR 的靈活性。頁面還是靜態生成,但可以設定過期時間。過期後,下個訪問時伺服器會在背景重新生成,用戶得到的仍然是舊的靜態頁面,但下次訪問就會看到新的了。

// app/products/[id]/page.tsx
export async function generateStaticParams() {
  const products = await fetch('https://abby.pretty.com/products').then(r => r.json())
  return products.map(p => ({
    id: p.id.toString()
  }))
}

export const revalidate = 3600

export default async function Product({ params }: { params: { id: string } }) {
  const product = await fetch(`https://abby.pretty.com/products/${params.id}`, {
    next: { revalidate: 3600 }
  }).then(r => r.json())

  return (
    <div>
      <h1>{product.name}</h1>
      <p>價格: {product.price}</p>
    </div>
  )
}

設定 revalidate 為 3600,意思是每個頁面都會在構建後 1 小時過期。過期後有人訪問,伺服器在背景重新運行這個組件的代碼,更新 HTML。舊的訪問者拿到舊頁面,新的訪問者拿到新的。

ISR 很聰明。大部分時間你拿到靜態速度,但內容並沒有那麼陳舊。電商網站、論壇、社交平台都適合。

性能與適用場景

CSR 首屏時間取決於網絡、JavaScript 大小、API 響應。通常 2 到 5 秒。轉化率吃虧。內部系統、高互動工具才用。

SSR 首屏時間取決於伺服器算力。一般 300 毫秒到 1 秒。但伺服器成本高,不能用 CDN。用戶多的時候吃虧。內容實時變化、需要登入狀態的用才用。

SSG 首屏時間從 CDN 返回,50 到 200 毫秒。最快。但內容更新慢,頁面數量多構建時間長。部落格、文檔、著陸頁才用。

ISR 首屏時間也是從 CDN,50 到 200 毫秒。內容更新不需要重新構建,1 小時或更短時間內會自動更新。電商、內容平台、會變但不是實時的資訊才用。

實際選擇

實際在商業應用時,很少會使用單一一個渲染模式,通常可能還會再加上一些 Client Side Components 的混合使用,形成 App Router 的混合渲染模式。下次再來聊聊 App Router 和 Pages Router,以及怎麼選擇兩個路由系統。