iT邦幫忙

2021 iThome 鐵人賽

DAY 3
2
Modern Web

從零開始學習 Next.js系列 第 3

Day03 - 深入淺出 CSR、SSR 與 SSG

前言

在這篇文章中,我們來聊聊 CSR、SSR 與 SSG 的不同,這三者皆是現今常被提到的網頁技術,而 Next.js 同時支援了這三種技術,我們可以在不同的頁面中選擇適合的方法實作。但是實作之前,勢必得先了解這三種技術的優劣,以及適合的情境,了解了之後才知道該在什麼樣的情況下選擇何種技術。

在認識這三種技術之前,你可能會對名詞有些疑惑,因為在網路上找資料,通常是 CSR、SSR 與 pre-rendering 三種,但這篇文章中將 pre-rendering 換成了 SSG 這是有什麼原因嗎?

這是 Next.js 在文件中對於 pre-rendering 有不同的解釋,Next.js 的官方認為 SSR 與 SSG 皆屬於 pre-rendering 的一種技術,聽起來其實也挺合理的,因為使用者都是能夠拿到已經渲染資料的 HTML 檔案,所以稱作 pre-rendering 也說得過去。

接下來,就讓我們來聊聊這三種技術吧!

CSR (Client Side Rendering)

CSR 如同其名,是將渲染資料的所有過程都交由瀏覽器端處理,使用者在瀏覽網站時,第一次跟伺服器請求的 HTML 檔裡面幾乎不包含任何的內容,伺服器並沒有傳入資料到 HTML。接著,後續會再透過載入的 bundle,也就是 JavaScript 的檔案,再讓 JS 執行 AJAX 跟伺服器請求資料,最後將資料渲染到畫面上。

以下是一個常見 CSR 的應用中,伺服器在使用者瀏覽網頁時會回傳的 HTML 檔案,在 HTML 中僅包含一個 <div> 的節點,在 bundle 載入後,JS 才會塞入資料到 <div> 的節點中。

<html>
    <head>
        <link href="/static/css/main.css" rel="stylesheet">
    </head>
    <body>
        <div id="app"></div>
    </body>
    <script src="/static/js/bundle.js"></script>
</html>

而且因為 bundle 需要包含呼叫 API 的程式碼,一般來說整體會檔案大小會比較大一點,因此載入的時間就會稍微長一些。在載入完之後,也需要呼叫 API 也是一段時間成本。

但是 CSR 也不是沒有優點,因為 bundle 在一開始就載入進來,後續渲染畫面就不用不斷地跟伺服器端交互,體感上會比 SSR 快,而且在切換頁面的使用者體驗也會更好。使用 CSR 對於伺服器來說,也會有較小的負擔。

SSR (Server Side Rendering)

在過去的時代,傳統的 SSR 像是 PHP (世界上最棒的語言) 透過伺服器端處理任何的資料,然後再直接編譯成 HTML 檔案,最後使用者看到的就是完整包含資料的 HTML。

但這種傳統的做法有一個缺點是在切換頁面時,瀏覽器的畫面很明顯地閃爍,大家應該都有經驗,在這種情況下瀏覽網站的使用者體驗 (UX) 不是很好。

後來因為 SPA 開始逐漸盛行,使用者切換頁面不必再因為畫面不斷閃爍,而感到體驗不佳,但因為資料都是在瀏覽器端由 AJAX 發送請求跟伺服器端拿資料,如此一來 google 的爬蟲就沒辦法拿到資料,對於有礙於 SEO。

因此,其中一個解決辦法就是 SSR,但是不像似過去傳統的 SSR,而是保有 SPA 換頁時不會閃爍的優點,還可以讓伺服器動態地注入資料到 HTML 的檔案中,讓瀏覽器端第一次請求拿到的 HTML 就已經包含所有的資料,因此 google 爬蟲也就可以順利地爬到網站中的內容。

這樣看起來 SSR 不是很棒嗎?網頁的資料都是動態的,而且使用者在看到瀏覽網頁時還不用等待 API 回來後,再透過 JS 渲染資料到畫面上,大部分的時候這種方式的使用者體驗更好。但問題就在於使用 SSR 就必須有個伺服器一直處理使用者的請求,一直產生有資料的 HTML,並送到瀏覽器端,這樣的工作對於伺服器來說是一個負擔。

像是部落格如果使用 SSR 就像是殺雞用牛刀的感覺,部落格的內容通常不會每分每秒都在改變,如果使用 SSR,伺服器還得消耗資源處理並產生有資料的 HTML,這樣不是很浪費嗎?所以接下來要介紹另一種技術 — SSG,可以解決這方面的問題。

SSG (Static Side Generation)

SSG 意味著所有的內容都在 bulid 的時候都打包進入檔案中,所以使用者在瀏覽網站時,就可以拿到完整的 HTML 檔案。優點除了可以有利於 SEO 之外,還有因為每次使用者拿到的 HTML 內容都不會變,所以還可以讓 HTML 被 cache 在 CDN 上,很適合用在資料變動較小的網站中,像是部落格、產品介紹頁這種應用中。

但使用 SSG 這項技術時,除了必須考量到頁面資料更新頻率的問題,再者要衡量隨著應用越來越大時,打包的時間也會隨之增長。

CSR、SSR 與 SSG 怎麼選?

SEO

當我們談到 SEO,我們必須要先了解 SEO 背後的流程是 crawling → indexing → ranking 三個階段,而另一件重要的事是「沒有渲染的內容是不會被加入到 indexing 的」。

在 google search engine 團隊不斷的努力,CSR 網站實際上是可以參與 SEO 的,並不像是第一次跟伺服器拿到 HTML 後,發現裡面只有一個 <div> 後,就不理這個網站了。

這必須談到 googlebot 有所謂 second wave of indexing 技術,如果一個網站有需要被渲染的需求,亦即像是等待 JavaScript 把內容渲染出來,而 googlebot 遇到這種情況會先丟到 render queue 裡面,等待有資源處理渲染任務後才會回來做這件事。

而另一個你需要知道的是 googlebot 每天都要處理數量非常龐大的網站,它必須要有些機制判斷有些內容實際上不必參與 SEO,也就是 render budget 的評估機制。

render budget 包括像是等待渲染的時間太久、很少人去的網站等因素,則會造成 googlebot 評估一個網站「不用等到全部的內容渲染完畢後才 indexing」,因此這種情況下只有部分內容會進入到 indexing 跟 ranking 的階段。所以對使用者來説,比較重要的內容是在 JavaScript 渲染階段才會出現在畫面上,googlebot 有機會不將這些重要的內容納入 indexing 中,最終將會不利於 SEO。

綜合上述,CSR 實際上可以參與 SEO,但是不利於內容變動快速的網站,因為 CSR 沒辦法讓 googlebot 快速地拿到需要的內容。這時候 SSR 跟 SSG 就能夠發揮效用,googlebot 不必經過 second wave of indexing 就可以迅速地跟伺服器拿到資料,因此有利於內容變動快速的網站做 SEO。

當然,以上只是說明了 SEO 的一小部分,根據 Google 工程師 Martin Splitt 在 JavaScript: SEO Mythbusting 這部影片中分享了 SEO 的其中一個秘辛是「它的指標有 200 多種」,上述只是稍微摸到邊而已,有興趣的人再深入去研究吧!?

更快地載入頁面內容

前面有提到 CSR 是在載入 bundle 後,才跟伺服器要資料,因此使用者會比較慢看到網站的內容。而對於 SSR 來說伺服器會在頁面中的資料都準備完畢後,才給使用者有資料的 HTML 檔案,因此在載入速度的體驗上是 SSR 更為快一些。而 SSG 則是在 build 的時候就已經產生資料,使用者在進入一個網站後,伺服器可以直接回應已經產生的 HTML 檔案,而且因為有 cache 的機制,載入速度整體上是三者中最快的。

此外,對於較舊、效能不足的設備,你能想像使用 CSR 的網站還要處理大量的 JavaScript 程式才能看到內容嗎?在這種情況下,把負擔轉移到伺服器身上,也許是可以考量的因素之一。

SSG v.s. SSR

這兩者都是讓伺服器直接回應包含完整內容的 HTML,該怎麼選擇呢?可以考慮到網站的內容是靜態的,還是需要透過 API 動態的拿到最新的資料。如果說內容是靜態的,SSG 的方式就非常的適合;但如果資料變動很頻繁,需要呼叫 API 拿到最新的資料,那就得依靠 SSR,但是 SSR 會影響伺服器的資源,所以有時候 SSG 跟 SSR 就是取捨的問題。

CSR 是可以捨棄的嗎?

如果說伺服器可以承受負擔,而且 SSG 跟 SSR 對於 SEO 從內容上來看都比較好,難道説「可以不用 CSR 嗎」。前面其實已經提到了「SEO 有 200 多項指標」,而如果全面使用 SSR,把所有資料都在伺服器端處理,其實也不一定有利於 SEO。

因為,頁面中的有些內容其實不必參與 SEO 的過程,SSR 只需把「對使用者有價值的資料」渲染完畢,把剩下的部分交由 CSR 處理,使用者可以更快地看到內容,有利於「First Contentful Paint」的評分。

Next.js 中的 CSR、SSR 與 SSG

在更深入了解了 CSR、SSR 與 SSG 後,接下來我們使用 Next.js 來一瞥這三者的差別。

在 Next.js 中要實作這三種模式語法會有點不一樣,以下為讀者必須知道的三個 function:

  • getStaticProps (SSG): 在 build 的時候抓取資料
  • getStaticPaths (SSG):在後面的章節,我們我聊到 Next.js 的 dynamic routes,這個 function 便是在這時候使用
  • getServerSideProps (SSR):在使用者進入網頁時,每一次發送請求伺服器都會抓取資料

至於沒有提到 CSR 是因為 React 搭配 react-router-dom 便是一種 CSR,而在 Next.js 中也是大同小異,但後面會再提到可以使用像是 useSWR 這種套件,讓抓取資料事半功倍。

接下來,我們就來玩玩看 Next.js 吧!這次我們將使用 ?JSONPlaceholder 這個網站提供的 fake API 。

建立專案

在第一天,我們已經使用過 create-next-app 創建了一個專案,如果沒有建立過的讀者,可以用以下指令建立一個空白專案:

yarn create next-app --typescript

創建完畢後,進入專案資料夾,然後啟動開發用伺服器:

yarn dev

Client Side Rendering (CSR)

CSR 的程式碼很單純,在 useEffect 中第一次 component mount 時使用 fetch 打 API,並將結果儲存到 useState 中。

import { useState, useEffect } from "react";

interface Post {
  userId: number;
  id: number;
  title: string;
  body: string;
}

export default function Home() {
  const [post, setPost] = useState<Post>();

  useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/posts/1")
      .then((res) => res.json())
      .then((res) => setPost(res));
  }, []);

  return (
    <div>
      <h1>{post?.title}</h1>
      <p>{post?.body}</p>
    </div>
  );
}

網站的內容很簡單,就只有 titlebody 而已。

各位可以打開網頁的原始碼,在網頁原始碼中,我們看不到 titlebody 的資料,只能看到一段空的標籤而已,這便是 CSR 的特性。

Server Side Rendering (SSR)

getServerSideProps

要在 Next.js 使用 SSR 要搭配 getServerSideProps 這個 function,在 component 外面會 export 一個非同步的 function,它執行完裡面的程式後,將 props 傳入到 component 裡面。

型別 Next.js 也已經幫我們定義好,只要從 next 中載入進來就可以使用。

import { GetServerSideProps } from "next";

export const getServerSideProps: GetServerSideProps = async () => {
  return {
    props: {},
  };
};

我們稍微改動 CSR 的程式碼,把 useStateuseEffect 從 component 中拿掉,改用從 getServerSideProps 傳入的 props 直接渲染到畫面上。

import { GetServerSideProps } from "next";

interface Post {
  userId: number;
  id: number;
  title: string;
  body: string;
}

interface HomeProps {
  post: Post;
}

export default function Home({ post }: HomeProps) {
  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
    </div>
  );
}

export const getServerSideProps: GetServerSideProps = async () => {
  const res = await fetch("https://jsonplaceholder.typicode.com/posts/1");
  const post: Post = await res.json();

  return {
    props: {
      post,
    },
  };
};

然後,我們同樣打該網頁中的原始碼來看看,你會發現原始碼中多了一些文字,不再像 CSR 只拿到一段空的 HTML。而讀者也可以注意到下方有個 __N_SSP ,代表這個網頁是用 server side props 的方式傳入資料。

Static Side Generation (SSG)

getStaticProps

要在 Next.js 使用 SSG 要搭配 getStaticProps 這個 function,在 component 外面會 export 一個非同步的 function,它執行完裡面的程式後,將 props 傳入到 component 裡面。

型別 Next.js 也已經幫我們定義好,只要從 next 中載入進來就可以使用。

要注意的是 getStaticProps 雖然可以在 next dev 開發者模式中使用,但它會跟 getServerSideProps 一樣都在使用者進入網頁 request 時被執行。

import { GetStaticProps } from "next";

export const getStaticProps: GetStaticProps = async () => {
  return {
    props: {},
  };
};

接著,我們看到 SSG 的範例程式,只要把前面 getServerSideProps 改成 getStaticProps 就可以觸發 SSG 了。

import { GetStaticProps } from "next";

// 忽略重複的程式碼

export const getStaticProps: GetStaticProps = async () => {
  const res = await fetch("https://jsonplaceholder.typicode.com/posts/1");
  const post: Post = await res.json();

  return {
    props: {
      post,
    },
  };
};

同樣地,SSR 跟 SSG 網頁原始碼中都已經包含了渲染過後的資料,不是一段空的 HTML 節點。讀可也可以看到下方有 __N_SSG 的屬性,表示這個網頁是用 static side generation 產生的。

Reference


上一篇
Day02 - 為什麼你需要 Next.js ?
下一篇
Day04 - Next.js 的 file-based routing
系列文
從零開始學習 Next.js30

1 則留言

0
Ken Chen
iT邦新手 5 級 ‧ 2021-09-18 17:11:26

感謝分享~
googlebot部分長知識了

我要留言

立即登入留言