根據前面的介紹,我們知道在 SSR 渲染模式中,如果伺服器遇到高流量請求時,會延後 TTFB 的時間;而 CSR 的缺點則是大型的 JavaScript bundle 可能會延後應用程式的 FCP、LCP 和 TTI 的時間,因為下載和處理肥大的 JavaScript 檔案需要時間,那是否有方法能解決這兩者的缺點呢?Static Rendering 或稱為 Static Site Generation(SSG) 試圖透過向客戶端提供預先渲染好的 HTML 來解決這些問題,接下來就來看看 Static Rendering 的運作吧~
home.html
home.html
home.js
home.js
內的 JavaScript 程式碼,以此水合(hydrate)頁面上的元件,讓元件可互動因為 SSG 已事先準備好 HTML,可減少 SSR 在伺服器處理請求、渲染 HTML 內容和回應請求花的時間,因此可更快回應使用者檔案,帶來更快的 TTFB 和 FCP,且因為 HTML 已事先渲染,能減少 JavaScript 程式碼大小,因此也可以更快的解析 JavaScript 來讓頁面具有互動性,TTI 較好。
SSG 流程示意圖如下。
圖 2 SSG 流程示意圖(資料來源:自行繪製)
Static Rendering 運作流程大多數與 Server Side Rendering 相似,兩者都會預先把 HTML 產生出來,兩者間最重要的差別是「靜態頁面(HTML)產生的時間點」:
附上 Static Rendering 和 Server-side Rendering 兩者的示意圖以供比較:
圖 3 Static Rendering 運作示意圖(資料來源:參考 https://nextjs.org/learn-pages-router/basics/data-fetching/two-forms 後自行繪製)
圖 4 Server-side Rendering 運作示意圖(資料來源:參考 https://nextjs.org/learn-pages-router/basics/data-fetching/two-forms 後自行繪製)
Static Rendering 會預先渲染 HTML 檔案,不像 SSR 會在每次使用者請求時,根據使用者身份而產出不同 HTML,因此 Static Rendering 更適合那種「不常更改、無論誰請求都顯示相同資料的頁面」,例如:關於我們、聯繫我們、部落格文章頁或產品說明頁面。
有支援 Static Rendering 的框架如:Next.js、Gatsby、Astro、Docusaurus、VitePress、Hugo。
以 Next.js 來說明 SSG 基本架構,以下是一個不包含動態資料的靜態頁面:
// pages/contact.js
export default function Contact() {
return (
<div>
<h1>Contact Us</h1>
<p>If you have any questions, feel free to reach out!</p>
{/* other contact information */}
</div>
);
}
當我們執行 next build
建構網站時,此頁面將被預渲染為一個 HTML 檔案 contact.html
,可透過路由 /contact
存取。
因為 HTML 檔案都預先渲染、建立好,所以搜尋引擎的爬蟲可從中建立索引,對 SEO 友善。
因為 HTML 檔案是預先被建立好,而不是使用者請求時才產生,所以伺服器的負擔較輕,有更快的 TTFB、FCP、TTI。
預渲染的 HTML 都是靜態內容,可以被 CDN 或 Vercel Edge Network 快取,能減少傳統 SSR 處理請求的時間。
對於整個應用程式的所有路徑,都要產生單獨的 HTML 文件,例如,使用 SSG 建立部落格應用程式時,資料庫中的每一篇文章都需要生成一個 HTML 文件,且如果文章需要修改時,都要重新 build 才能將更新的內容反應在靜態 HTML 中。要管理大量的 HTML 檔案可能會變得困難、有挑戰性。
當網頁內容有更改時,SSG 網站需要重新 build 和重新部署,如果網站沒有在內容更改後 build 和部署,顯示的網頁內容可能會變得過時,高度動態內容對 SSG 來說較不好管理,除非使用 SSG 的變體,否則 SSG 不適合有太多動態內容的網站。
可被快取是優點也可能是缺點,當網頁內容變多、HTML 檔案變多,CDN 快取就會需要花費更多機器成本。
純粹的靜態渲染(例如上面的 contact
範例)不涉及任何動態、會變動的資料。如果要在 SSG 加入動態資料,可考慮以下方式。
(以下將純粹、沒有動態資料的靜態渲染稱為「一般 SSG」,以便和其他有動態資料的 SSG 變體區別)
對於靜態資料與畫面使用靜態渲染預先渲染好,而預計要放動態資料的位置則先使用骨架元件(skeleton component)來渲染,等到客戶端載入頁面後,客戶端再發送 API 去取動態資料以填入頁面中。
示意圖如下,不同於一般 SSG 可擁有幾乎同時的 FCP 和 LCP,Client-Side fetch 的 FCP 和 LCP 會有點距離,因為客戶端需要等 API 資料回來才能渲染畫面。
圖 5 Static Rendering with Client-Side fetch 流程示意圖(資料來源:自行繪製)
因應上述缺點,Next.js 提供了一些解決方案如下,以改善 SSG 處理動態資料時的效能。
可透過在 Next.js 頁面元件(page component)中 export getStaticProps()
函式來達成。getStaticProps()
會在建構(build)階段於伺服器端執行,並將回傳的 props 傳遞給頁面元件,讓頁面元件使用這些 props 中的資料來預先渲染頁面。
因為 getStaticProps()
是在 build 階段執行的,所以它不會包含在客戶端的 JavaScript bundle 中。此外,因為 getStaticProps()
是在伺服器端執行的,它也可以用來直接從資料庫拿取資料。
// pages/articles.js
// 1. 這個函式在 build 伺服器的 build 階段執行,會從資料庫取得所有 article 資料並回傳
export async function getStaticProps() {
return {
props: {
articles: await getArticlesFromDatabase(),
},
};
}
// 2. 頁面元件在 build 階段從 getStaticProps 收到 articles prop,以此渲染頁面
export default function Articles({ articles }) {
return (
<>
<h1>Articles</h1>
<ul>
{articles.map((article) => (
<li key={article.id}>{article.title}</li>
))}
</ul>
</>
);
}
取得資料並渲染頁面元件的流程示意圖如下。
圖 6 取得資料並渲染頁面元件流程示意圖(資料來源:自行繪製)
整個請求的過程示意圖如下,當使用者請求頁面時,過程看起來類似一般 SSG,HTML 可被 CDN 緩存,瀏覽器會渲染 HTML 然後抓取需要的 JavaScript bundle 以進行 hydration。從瀏覽器角度來看,網路和 main thread 的運作也都和一般 SSG 相同,因此可擁有與一般 SSG 類似的效能。
圖 7 使用資料庫動態資料,靜態產生列表頁流程示意圖(資料來源:自行繪製)
可解決 Static Rendering with Client-Side fetch 的缺點,因為此方法可避免在客戶端發送 API 來請求資料,且資料載入時不需要骨架元件,因為頁面會直接渲染數據,同時客戶端會收到已渲染好的畫面,避免佈局偏移。
getStaticProps
方法,導致 build 時間變長可在 Next.js 用動態路由搭配 getStaticPaths()
函式來達成。
首先先建立一個共用的頁面元件 articles/[id].js
,[id]
在 Next.js 就是動態路由的意思。建立元件後,在其中 export getStaticPaths()
函式,getStaticPaths()
函式會回傳所有可能的 article id
,例如有 110
、111
、112
的 article,就會回傳告訴 Next.js 這個應用程式會有 articles/110
、articles/111
、articles/112
這些路徑。
在 build 階段時,Next.js 就會根據這些路徑去一一預渲染這些頁面,去渲染 articles/110
、articles/111
、articles/112
這些頁面並產生對應的 HTML 檔案。
// pages/articles/[id].js
// 1. 這個函式在 build 階段被執行,用來回傳一個包含所有要預先生成的動態路由(paths)頁面的列表。在這裡 getStaticPaths 會從資料庫中取得所有文章,並生成一個包含所有文章 id 的 paths 陣列
export async function getStaticPaths() {
const articles = await getArticlesFromDatabase();
const paths = articles.map((article) => ({
params: { id: article.id },
}));
// fallback: false 代表如果使用者請求的文章 id 不在列出的資料中,代表 id 不符合,則頁面會顯示 404
return { paths, fallback: false };
}
// 2. getStaticProps():當 Next.js 根據 getStaticPaths() 回傳的 paths 建置每個靜態頁面時,會自動將對應路徑中的參數(例如 id)傳遞給 getStaticProps() 的 params 參數,getStaticProps 再根據這個參數帶的 article id 來取出文章資料,然後傳給頁面元件做渲染
// 舉例來說,當 Next.js 需要預先生成 /articles/110 頁面時,getStaticProps 的 params 將包含 { id: "110" },getStaticProps 可以利用這個 id 來取得特定文章的資料,並將其作為 props 傳遞給該頁面元件
export async function getStaticProps({ params }) {
return {
props: {
article: await getArticleFromDatabase(params.id),
},
};
}
export default function Article({ article }) {
// 渲染文章資訊
}
這種方式適合的情境是詳細資訊的頁面,例如產品或部落格頁面,這類詳細頁面通常會有某些固定模板,然後再將動態資料填入佔位符(placeholder)中,實作時就是將固定模板和動態資料合併,為每個詳細頁面產生個別頁面。
SSG 的渲染方式會預先渲染好所有路由需要的 HTML 檔案,當檔案變多以後就會變得較難維護,且每次要更新內容時就要重新 build,耗費大量 build 成本,因此後來發展出 SSG 的優化方案 Incremental Static Generation(或稱 Incremental Static Regeneration,ISR),將在下篇介紹。