作者 | 逸恆
打開 next.js 官網,首先映入眼簾的是它的 Slogan 和介紹:
The React Framework for Production
Next.js gives you the best developer experience with all the features you need for production: hybrid static & server rendering, TypeScript support, smart bundling, route pre-fetching, and more. No config needed.
Next.js 提供了生產環境所需的所有功能以及最佳實踐,包括構建時預渲染、服務端渲染、路由預加載、智能打包、零配置等。其中,Next.js 以其優秀的構建時渲染和服務端渲染能力,成為當今 React 生態中最受歡迎的框架之一。本文將介紹 Next.js 提供的三種預渲染模式以及混合渲染模式,來看看 Next.js 是怎麼做預渲染的。
三種預渲染模式
普通的單頁應用只有一個 HTML,初次請求返回的 HTML 中沒有任何頁面內容,需要通過網絡請求 JS bundle 並渲染,整個渲染過程都在客戶端完成,所以叫客戶端渲染(CSR)。這種渲染方式雖然在後續的頁面切換速度很快,但是也明顯存在兩個問題:
- 白屏時間過長:在 JS bundle 返回之前,頁面一直是空白的。假如 bundle 體積過大或者網絡條件不好的情況下,體驗會更不好
- SEO 不友好:搜索引擎訪問頁面時,只會看 HTML 中的內容,默認是不會執行 JS,所以抓取不到頁面的具體內容
而 Next.js 提供的三種預渲染模式,均在 CSR 開始前,在服務端預先渲染出頁面內容,避免出現白屏時間過長和 SEO 不友好的問題。
SSR
為了解決上面出現的兩個問題,SSR(Server Side Rendering)誕生了。相信大家對 SSR 不會陌生,它是在服務端直接實時同構渲染當前用戶訪問的頁面,返回的 HTML 包含頁面具體內容,提高用戶的體驗。React 從框架層面直接提供支持,只需要調用 renderToString(Component)
函數即可得到 HTML 內容。
Next.js 提供 getServerSideProps
異步函數,以在 SSR 場景下獲取額外的數據並返回給組件進行渲染。getServerSideProps
可以拿到每次請求的上下文(Context),舉個例子:
export default function FirstPost(props) {
// 在 props 中拿到數據
const { title } = props;
return (
<Layout>
<h1>{title}</h1>
</Layout>
)
}
export async function getServerSideProps(context) {
console.log('context', context.req);
// 模擬獲取數據
const title = await getTitle(context.req);
// 把數據放在 props 對象中返回出去
return {
props: {
title
}
}
}
SSR 方案雖然解決了 CSR 帶來的兩個問題,但是同時又引入另一個問題:需要一個服務器承載頁面的實時請求、渲染和響應,這無疑會增大服務端開發和運維的成本。另外對於一些較為靜態場景,比如博客、官網等,它們的內容相對來說比較確定,變化不頻繁,每次通過服務端渲染出來的內容都是一樣的,無疑浪費了很多沒必要的服務器資源。這時,有沒有一種方案可以讓這些頁面變得靜態呢?這時,靜態站點生成(SSG,也叫構建時預渲染)誕生了。
SSG
SSG(Static Site Generation) 是指在應用編譯構建時預先渲染頁面,並生成靜態的 HTML。把生成的 HTML 靜態資源部署到服務器後,瀏覽器不僅首次能請求到帶頁面內容的 HTML ,而且不需要服務器實時渲染和響應,大大節約了服務器運維成本和資源。
Next.js 默認為每個頁面開啟 SSG。對於頁面內容需要依賴靜態數據的場景,允許在每個頁面中 export
一個 getStaticProps
異步函數,在這個函數中可以把該頁面組件所需要的數據收集並返回。當 getStaticProps
函數執行完成後,頁面組件就能在 props
中拿到這些數據並執行靜態渲染。舉個在靜態路由中使用 SSG 的例子:
// pages/posts/first-post.js
function Post(props) {
const { postData } = props;
return <div>{postData.title}</div>
}
export async function getStaticProps() {
// 模擬獲取靜態數據
const postData = await getPostData();
return {
props: { postData }
}
}
對於動態路由的場景,Next.js 是如何做 SSG 的呢?Next.js 提供 getStaticPaths
異步函數,在這個方法中,會返回一個 paths
數組,這個數組包含了這個動態路由在構建時需要預渲染的頁面數據。舉個例子:
// pages/posts/[id].js
function Post(props) {
const { postData } = props;
return <div>{postData.title}</div>
}
export async function getStaticPaths() {
// 返回該動態路由可能會渲染的頁面數據,比如 params.id
const paths = [
{
params: { id: 'ssg-ssr' }
},
{
params: { id: 'pre-rendering' }
}
]
return {
paths,
// 命中尚未生成靜態頁面的路由直接返回 404 頁面
fallback: false
}
}
export async function getStaticProps({ params }) {
// 使用 params.id 獲取對應的靜態數據
const postData = await getPostData(params.id)
return {
props: {
postData
}
}
}
當我們執行 nextjs build
後,可以看到打包結果包含 pre-rendering.html
和 ssg-ssr.html
兩個 HTML 頁面,也就是說在執行 SSG 時,會對 getStaticPaths
函數返回的 paths
數組進行循環,逐一預渲染頁面組件並生成 HTML。
├── server
| ├── chunks
| ├── pages
| | ├── api
| | ├── index.html
| | ├── index.js
| | ├── index.json
| | └── posts
| | ├── [id].js
| | ├── first-post.html
| | ├── first-post.js
| | ├── pre-rendering.html # 預渲染生成 pre-rendering 頁面
| | ├── pre-rendering.json
| | ├── ssg-ssr.html # 預渲染生成 ssg-ssr 頁面
| | └── ssg-ssr.json
SSG 雖然很好解決了白屏時間過長和 SEO 不友好的問題,但是它僅僅適合於頁面內容較為靜態的場景,比如官網、博客等。面對頁面數據更新頻繁或頁面數量很多的情況,它似乎顯得有點束手無策,畢竟在靜態構建時不能拿到最新的數據和無法枚舉海量頁面。這時,就需要增量靜態再生成(Incremental Static Regeneration)方案了。
ISR
Next.js 推出的 ISR(Incremental Static Regeneration) 方案,允許在應用運行時再重新生成每個頁面 HTML,而不需要重新構建整個應用。這樣即使有海量頁面,也能使用上 SSG 的特性。一般來說,使用 ISR 需要 getStaticPaths
和 getStaticProps
同時配合使用。舉個例子:
// pages/posts/[id].js
function Post(props) {
const { postData } = props;
return <div>{postData.title}</div>
}
export async function getStaticPaths() {
const paths = await fetch('https://.../posts');
return {
paths,
// 頁面請求的降級策略,這裡是指不降級,等待頁面生成後再返回,類似於 SSR
fallback: 'blocking'
}
}
export async function getStaticProps({ params }) {
// 使用 params.id 獲取對應的靜態數據
const postData = await getPostData(params.id)
return {
props: {
postData
},
// 開啟 ISR,最多每10s重新生成一次頁面
revalidate: 10,
}
}
在應用編譯構建階段,會生成已經確定的靜態頁面,和上面 SSG 執行流程一致。
在 getStaticProps
函數返回的對象中增加 revalidate
屬性,表示開啟 ISR。在上面的例子中,指定 revalidate = 10
,表示最多10秒內重新生成一次靜態 HTML。當瀏覽器請求已在構建時渲染生成的頁面時,首先返回的是緩存的 HTML,10s 後頁面開始重新渲染,頁面成功生成後,更新緩存,瀏覽器再次請求頁面時就能拿到最新渲染的頁面內容了。
對於瀏覽器請求構建時未生成的頁面時,會馬上生成靜態 HTML。在這個過程中,getStaticPaths
返回的 fallback
字段有以下的選項:
fallback: 'blocking'
:不降級,並且要求用戶請求一直等到新頁面靜態生成結束,靜態頁面生成結束後會緩存fallback: true
:降級,先返回降級頁面,當靜態頁面生成結束後,會返回一個 JSON 供降級頁面 CSR 使用,經過二次渲染後,完整頁面出來了
在上面的例子中,使用的是不降級方案(fallback: 'blocking'
),實際上和 SSR 方案有相似之處,都是阻塞渲染,只不過多了緩存而已。
If fallback is 'blocking', new paths not returned by getStaticPaths will wait for the HTML to be generated, identical to SSR (hence why blocking), and then be cached for future requests so it only happens once per path.
也不是所有場景都適合使用 ISR。對於實時性要求較高的場景,比如新聞資訊類的網站,可能 SSR 才是最好的選擇。
混合渲染模式
Next.js 不僅支持 SSR、SSG、CSR、ISR,還支持渲染模式的混合使用。下面將介紹三種混合渲染模式。
SSR + CSR
上面已經提及過,SSR 似乎已經解決了 CSR 帶來的問題,那是不是 CSR 完全沒有用武之地呢?其實並不是。使用 CSR 時,頁面切換無需刷新,無需重新請求整個 HTML 的內容。既然如此,可以各取所長,各補其短,於是就有 SSR + CSR 的方案:
- 首次加載頁面走 SSR:保證首屏加載速度的同時,並且滿足 SEO 的訴求
- 頁面切換走 CSR:Next.js 會發起一次網絡請求,執行
getServerSideProps
函數,拿到它返回的數據後,進行頁面渲染
二者的有機結合,大大減少後端服務器的壓力和成本的同時,也能提高頁面切換的速度,進一步提升用戶的體驗。除了 Next.js,還有其他的框架也使用 SSR + CSR 方案,比如 ice.js 等。
SSG + CSR
在上面已提及過,SSR 需要較高的服務器運維成本。對於某些靜態網站或者實時性要求較低的網站來說,是沒有必要使用 SSR 的。假如用 SSG 代替 SSR,使用 SSG + CSR 方案,是不是會更好:
- 靜態內容走 SSG:對於頁面中較為靜態的內容,比如導航欄、佈局等,可以在編譯構建時預先渲染靜態 HTML
- 動態內容走 CSR:一般會在 useEffect 中請求接口獲取動態數據,然後進行頁面重新渲染
雖然從體驗來說,動態內容需要頁面重新渲染後才能出現,體驗上沒有 SSR 好,但是避免 SSR 帶來的高額服務器成本的同時,也能保證首屏渲染時間不會太長,相比純 CSR 來說,還是提升了用戶體驗。
SSG + SSR
在上面介紹的 ISR 方案時提及過,ISR 的實質是 SSG + SSR:
- 靜態內容走 SSG:編譯構建時把相對靜態的頁面預先渲染生成 HTML,瀏覽器請求時直接返回靜態 HTML
- 動態內容走 SSR:瀏覽器請求未預先渲染的頁面,在運行時通過 SSR 渲染生成頁面,然後返回到瀏覽器,並緩存靜態 HTML,下次命中緩存時直接返回
ISR 相比於 SSG + CSR 來說,動態內容可以直接直出,進一步提升了首次訪問頁面時的體驗;相比於 SSR + CSR 來說,減少沒必要的靜態頁面渲染,節省了一部分後端服務器成本。
總結
本文首先介紹了 Next.js 提供的三種預渲染模式:SSR、SSG、ISR,並分別說明了它們的優缺點以及可能適用於哪些場景。後面介紹了 Next.js 目前支持的三種混合渲染模式,並和其他的渲染模式進行對比。
總的來說,沒有十全十美的渲染方案,都需要根據實際場景進行權衡和取捨。
參考鏈接:
Next.js 官方文檔:https://nextjs.org/
《從 Next.js 看企業級框架的 SSR 支持》
《新一代 Web 建站技術棧的演進:SSR、SSG、ISR、DPR 都在做什麼?》:https://zhuanlan.zhihu.com/p/365113639