初探 Server-Side-Rendering 與 Next.js

最近在 Frontend 技術圈裡,Server-Side-Rendering(SSR) 可說是一個越來越火熱的概念,剛好現在工作上的架構就是使用 Next.js(React 的 SSR 框架)開發,過程中發現滿多有趣與讓自己驚豔的地方,因此想花一個篇幅紀錄 SSR 的基本概念,還有介紹 Next.js 擁有什麼樣的特色,可以讓它成為當今 React 最熱門的 SSR 框架。

(因為本人習慣使用 React,因此以下的範例或概念都會用 React 撰寫)

在講 Server-Side-Rendering 之前,我想先談談與之相對應的概念: Client-Side-Rendering (CSR)。在前端框架(React、Vue、Angular)盛行後,SPA (Single-Page-Application) 就如雨後春筍般冒出,而它的運作概念是這樣的:

以往會被塞滿各種 element 的 HTML 檔,如今只會放入一個 tag 當作「容器」而已,例如以下範例

<html>
<head></head>
<body>
<div id="root"></div>
<script src="./script.js"></script>
</body>
</html>

上面 id 為 root 的 div 即成為了上面提到的容器,script.js 則是 react code ,採用這種方式後,頁面的元素都將交由 react 去渲染出來並塞進容器中。

圖片來源

server 回傳只包含容器的 HTML -> 瀏覽器依據 HTML 下載 JS code -> 瀏覽器執行 React code -> 執行完畢後,頁面才完整呈現與具有互動型

採用這種方式後,我們可以不必像以前ㄧ樣準備ㄧ大堆的 HTML 檔案,而是透過 Router 決定該渲染出哪些元素在畫面上或是該抓哪些 API data,而 routing 的過程也不再像以往ㄧ樣要重新去 server 抓取頁面造成頁面反白,使用者體驗大大的提升,這就是 Client-Side-Rendering。

看似美好,然而,它也產生了一些問題 。

SEO 對很多企業網站來說是很重要的指標,而它很大一部分得靠搜尋引擎的爬蟲爬取網站的資訊,問題出在上面說的,我們的 HTML 現在只有容器在裡面而已,其他內容都是由 JS 動態產生的,這會造成爬蟲在爬取資料時只會爬到空蕩蕩的幾個 tag…,也因此造成 SEO 分數低落。(有興趣的讀者可以在想觀察的網頁點選右鍵:檢查網頁原始碼)。

(話說 google 搜尋引擎越來越強了,未來連 CSR 頁面都能夠用新技術爬到。)

上面提到 CSR 的步驟是瀏覽器載入 JS 後再去執行它,最後靠 JS 才能渲染出要顯示的元件與交互,但是如果你的 JS 隨著專案的擴充變得非常肥大呢?瀏覽器在下載 JS 與執行 JS 上所花的時間都會因此增加,儘管現在瀏覽器已經非常快速了,仍然會對效能造成影響,而因為 CSR 是得在 JS 執行完畢時才能顯示出整個網頁,上述流程的 delay 連帶的影響到使用者等待頁面顯示的時間,據分析指出,如果網頁載入時間從 1 秒增加到 3 秒,跳出率就會增加 32%,這對於電商類型的網站來說影響營收可是很可觀的。(當然 CSR 也有 code-spliting、dynamic-import 等解決 bundle size 過於肥大的方案,但這不在本篇文章的討論範圍中。)

要解決 SEO 的問題,通常會採用 pre-rendering 的方式,所謂 pre-rendering 一般來說是指在送到 client 端以前就把 HTML 渲染出來,常見的 pre-rendering 有兩種方式:

  • Static Site Generation (SSG)
  • Server Side Rendering (SSR)

意指 HTML 在 build time 時就由 server 產生好,並且在之後的 client request 都會共用這個 HTML。採用這種方式的優點為效能方面,因為它有較好的快取機制,不過它的限制也十分明顯,因為頁面資料的抓取(如 API call)只能透過 application build 的時候,因此比較適合使用在內容不需要經常變換的頁面上。

沒想到到這裡才要來介紹本篇的主角 SSR,SSR 與 SSG ㄧ樣都是在 server 先建立好要回傳的 HTML 內容,然而不同的是 SSR 在對該頁面的每一個 request 都會重新抓取,也就是說它是與 SSG 的 static 相對應的 dynamic 形式。每當有 client 發起對某個頁面的 request 時,server 會抓取對應的資料,建立完整的 HTML,最後再回傳給 client。

圖片來源

所以 SSR 理所當然可以解決 SEO 的問題,爬蟲爬到的都是從 server 建立好並帶上資訊的完整 HTML。接下來看看上面的圖,這是 SSR 的架構模型,其中與 CSR 架構最大的差別是 SSR 不必等到 JS 執行完畢後才能讓使用者看到畫面,在 server 回傳 HTML 後使用者就能夠看到頁面,即使因為 JS 還沒被執行,所以畫面還不具備互動性,但讓使用者先看到畫面,再利用人的感知延遲時間去執行 JS code,可以大大減少使用者的跳出率,於是上面提到的第二個問題也被解決了。

嗯…這次總算完美了吧…..

不,SSR 的缺點(應該算吧?)是架構與邏輯會變得十分複雜,許多程式會需要分別在 server 與 client 做處理,也要特別小心不能在 server 端執行到包含如 window 等 browser 的 API,總之我認為會將專案複雜度往上提高一個層級。

除了上述提到的 SSG 與 SSR 以外,要解決爬蟲的 SEO 問題,還是有其他解決方案的,畢竟如果已經是個達到一定規模的 CSR 專案,突然要轉到 SSR 好像也滿困難的,之前實習公司的主管最近就用了蠻潮的方式(AWS Lambda)解決了 CSR 架構 pre-rendering 的問題,有興趣的讀者可以看看這篇文章

圖片來源: developers.google.com

Next.js

今天不是要來講 Next.js 怎麼使用,畢竟網路上學習資源已經一堆了,也絕對不是一兩篇文章就可以講解完的,今天是想透過這篇文章展現 Next.js 的強大與方便,希望能夠推坑更多人進入 Next.js 的世界。

Next.js 是 React 的 SSR 框架,Vue 也有類似的 SSR 框架叫做 Nuxt,如果要一句話解釋它們為什麼存在,我會說:

因為自己實作 SSR 真的太痛苦了!

如果想要看看自己實作 SSR 會長什麼樣子的話可以參考以下的 Repo,是我參考網路學習資源建立的 React + Redux + React-Router 的 SSR project,只能說心真的太累了…

接下來的內容會假設讀者是對 React 有基本了解的,一一介紹 Next.js 的特性。

這是 Next.js 最大的特色之一,Next.js 會將在 page 這個資料夾下的檔案自動做 routing,例如 page/mypage.tsx 這樣的檔案路徑,就會自動顯示在 localhost:3000/mypage,以往需要自己處理的 routing,現在透過檔案架構就能輕鬆管理了,Next.js 甚至還支援 dynamic routing 呢!例如 page/[name].tsx,這樣無論是 localhost:3000/harry 或是 localhost:3000/kyle 都會被帶到上面那個檔案裡喔!

上個段落提及的 SSG 也能夠在 Next.js 實現喔!

詳情請見 Next.js Data Fetching Session

前面有提及 SSR 會在 server 先抓取必要的資料,產生 HTML 再回傳給 client,而 Next.js 則提供許多不同種抓資料的方式。

  • getInitialProps:

getInitialProps 的主要功能為在載入頁面之前,異步的去抓取需要的資料,並將資料變為該 Page Component 的 props,它的執行順序會在所有 react component lifecycle 之前。初學者比較容易搞混的則是它的執行環境分為在「client side」與 「server side」。初次載入頁面的時候 getInitialProps 會在 server side 執行,當使用 Next/Link | Next/Router 進行跳轉時,它則會在 client side 執行,而如果跳轉頁面不是使用上面的方式,getInitialProps 都是會在 server-side 執行喔!如果是 CSR 的 React 架構,往往是在 useEffect 去執行 API call,但在 Next.js 架構中,許多 API call 會被拉到 getInitialProps 執行(當然還是得看需求決定)。

  • getServerSideProps

getServerSideProps 是一個比較新的概念,它與 getInitialProps 不同的地方在於它只會在 server side 執行,這也就代表著有了它我們是可以直接跟 DB 溝通的,反正它就是一段跑在 server 的 code,而這也就代表在某些狀況下我們可以跳過 API call 這個步驟,直接去資料庫抓取資料。

看似美好但它並不適用在每個情境,Next.js 官方是這麼敘述它的使用時機的:

You should use getServerSideProps only if you need to pre-render a page whose data must be fetched at request time. Time to first byte (TTFB) will be slower than getStaticProps because the server must compute the result on every request, and the result cannot be cached by a CDN without extra configuration.If you don’t need to pre-render the data, then you should consider fetching data on the client side.

因為它是一個相對新的概念,適用情境也較不符合我們公司的產品,因此目前公司專案尚未使用到它,不過也許未來會有什麼新的發展,持續觀察下去吧!有興趣可以參考這裏(話說一個 file 裡面有前端也有後端 code 還是我寫 react 以來第一次遇到呢!)

  • getStaticProps && getStaticPaths

專門用來處理 SSG 需求,有興趣的讀者可以參考這篇文章

如果有需要在 Next.js 專案中建立 backend server 的需求, 一般來說有兩種方式:

  • custom server : 像我們公司的專案即是使用 Koa.js 做客製化的 backend server,負責處理 authentication 、proxy、routing 等較貼近 client 端的事務。
  • serverless API Routes‌ : 接下來提到的 API Routes,最終會被 Next.js deploy 成 serverless function。

當我看到這個 feature 時,真的覺得十分振奮人心啊!Next.js API Routes 讓我們可以透過將 API handler function 放入 api folder 的檔案中,Next.js 會自動幫你實作 API 的 routing。

/api/person/[id]/index.ts

例如上面的範例,Next.js 會為我們產生 route 為 /person/[id] 的 API route,然後我們就可以在上面講到的 data fetching 中抓取這個 API 取得我們要的資料。到現在看我還是覺得太神奇了,也就是說我們在 Next.js 的專案中除了以往處理 render server,現在也能夠處理 backend API server,並且透過這種模式建立的 API 是易於維護及擴展的。

這篇文章首先介紹了 SSR 的基本觀念,並與 CSR、SSG 也做了一些比較,再來介紹一些 Next.js 框架的特色,希望能夠吸引更多人入坑,以我的觀點而言 Next.js 是變得越來越強大的,API Route 的到來也將開發增加了更多可能性,因此我相信持續關注它甚至是整個 SSR 架構是不會吃虧的,希望這篇文章對讀者能夠有所幫助。

--

--

一群技術人想要寫出一些好文章所建立的技術專欄。每週二一篇原創文章、一封電子報,歡迎大家訂閱!主網站: https://weekly.starbugs.dev/。

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store