iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0
Vibe Coding

《我與 AI 的奇幻漂流:30 天,把「能跑」變成「能上線」》系列 第 20 篇

【Day 20|捕魚的結果】網頁在哪裡被做出來?渲染方式、搜尋與資料狀態

  • 分享至 

  • xImage
  •  

昨天做完收藏之後,回頭看了一下這幾天做的功能,發現一件事:同樣是從 Supabase 拿資料,專輯列表、搜尋、我的收藏,處理方式似乎有些不太一樣。有些在伺服器完成,有些交給瀏覽器。

或是說像搜尋,輸入「aespa」,我只打了一個字母就出現了篩選的結果,網站真的每打一個字母,就去資料庫查一次嗎?

在電影裡,有一晚飛魚群直接撞進救生艇,Pi 根本不用撒網,魚已經在船上了。

網站也是。要找東西之前,可以先去思考:資料是還在海裡,還是早就在船上了?


一、網頁在哪裡被做出來?

使用者看到的每一個畫面,都要經過兩件事:拿資料,和把資料組成畫面。這兩件事在哪裡做、什麼時候做,都有許多不同的選擇。

先認識確認三個角色:

角色 在這個專中是什麼
瀏覽器(前端) 使用者的 Chrome、Safari
Next.js 伺服器(後端) 執行 Server Component、組頁面的地方;部署後會在 Vercel 上
Supabase Data API 加上資料庫

1.1 SSR(Server-Side Rendering,伺服器端渲染):每次請求都現做

使用者打開網頁時,請求先送到伺服器。伺服器去資料庫拿資料、組成完整的 HTML,再將完整的HTML 送回瀏覽器。

https://ithelp.ithome.com.tw/upload/images/20261004/20178017aN2PdOwxMT.png
【圖 1|SSR 時序圖】

使用者拿到的會是完整的頁面,每次請求都要由伺服器產生;至於資料是否每次重查,還要看快取的設定。這個專案目前的實作中,首頁和專輯列表頁目前就是這種。

這是 SSR 的基本流程。Next.js 也可以先送出已經準備好的部分,比較慢的區塊先顯示載入畫面,之後再補上,這叫做串流(Streaming);後面的 loading.tsx 就跟它有關。

通常比較適合:每個人看到的不一樣,或資料需要即時,又希望搜尋引擎看得到內容的頁面,例如帳號頁、商品價格。代價是每次請求都要伺服器處理,負擔和等待時間有時候會比較多。

1.2 CSR(Client-Side Rendering,客戶端渲染):瀏覽器自己來

伺服器會先送出頁面的空殼並執行程式;瀏覽器執行程式後,再去拿資料、組出畫面。

https://ithelp.ithome.com.tw/upload/images/20261004/20178017K6X5faB2Km.png
【圖 2|CSR 時序圖】

這是典型的 CSR 流程。目前專案中的收藏清單就是由瀏覽器自己去拿:訪客的收藏存在瀏覽器裡;登入後,瀏覽器透過 Data API 取得帳號收藏,由 RLS 把關。但收藏頁的專輯目錄仍由伺服器先載入,所以整頁其實是混合的。

比較適合:每個人不一樣、互動多、不太需要被搜尋到的畫面,例如個人收藏、管理後台。代價是第一眼要等程式和資料都到了才看得到內容,想被搜尋到的內容也要另外注意。

1.3 SSG(Static Site Generation,靜態網站生成):事先做好

在 build 時,就先把資料拿好、把頁面做好。使用者來的時候,直接拿現成的頁面,不用再查資料庫。

build 是把程式整理成正式版本的步驟;部署到 Vercel 時,會先 build 再上線。所以 SSG 的頁面,通常是在每次部署時重新產生。

https://ithelp.ithome.com.tw/upload/images/20261004/201780179HbzBBiaGI.png
【圖 3|SSG 時序圖】

SSG 也不一定只在 build 時產生:像專輯詳情這種動態路由,可以只在 build 時先產生部分網址,其他網址也可能在第一次有人造訪時才產生,要看路由的設定。

比較適合:大家看到的都一樣、很少變動的內容,例如「關於我們」、說明文件。代價是資料改了,頁面不會自動跟著變,通常要重新 build、重新部署。

1.4 ISR(Incremental Static Regeneration,增量靜態再生成):現成的,但會更新

ISR 可以看成 SSG 的延伸:平常像 SSG 一樣,直接送出現成的頁面;需要更新時,才像 SSR 一樣,由伺服器重做一次。

具體來說,一樣先把頁面做好,但設定一個有效時間。超過時間後,下一個請求進來時才會觸發重做:有效時間內造訪的訪客會先拿到現有存在的那份,之後的人才拿到新的。也可以在資料更新時,由程式主動要求重做。

https://ithelp.ithome.com.tw/upload/images/20261004/20178017n8gHXIge5s.png
【圖 4|ISR 時序圖】

比較適合:大家看到的都一樣、偶爾會改的內容,例如部落格文章、商品介紹。代價是資料更新後,可能有一小段時間還看得到舊的版本。

1.5 怎麼選?

四種方式各有取捨,沒有哪一種一定最好,也不一定只能用一種,可以混合。選擇時,通常可以先問三個問題:

問題 方向
每個人看到的一樣嗎? 不一樣(例如有登入狀態、個人資料)時,通常會考慮 SSR 或 CSR
資料多久變一次? 幾乎不變可以考慮 SSG;偶爾變可以考慮 ISR;需要即時則傾向 SSR 或 CSR
需要被搜尋引擎找到嗎? 需要的話,比較不適合只靠 CSR

https://ithelp.ithome.com.tw/upload/images/20261004/20178017GT3qcKzysE.png
【圖 5|選擇渲染方式的參考流程】

這只是一個起點。實際上還要考慮資料量、伺服器成本、團隊熟悉度;同一個網站裡,不同頁面的答案也可能不一樣。

這些不是 Next.js 才有的概念。早期用 PHP、Django 寫的網站大多是 SSR;早期的 React、Vue 單頁應用程式是 CSR;部落格常用的 Hugo、Jekyll 則是 SSG。Next.js 的特色,是讓同一個專案裡的每一頁,都可以選不同的方式。

搜尋引擎看的是 HTML

SSR、SSG、ISR 送出去的 HTML 裡就有完整內容;CSR 一開始只有空殼,內容要等 JavaScript 跑完才出現。Google 會執行 JavaScript,但可能比較晚才處理;很多社群平台產生分享預覽時則不會執行。所以想被搜尋到的內容,最好一開始就在 HTML 裡。

目前的專輯詳情內容由伺服器產生,HTML 裡已經有完整內容。搜尋結果頁(?q=...)通常不需要被收錄,所以搜尋在瀏覽器過濾,不影響 SEO。


二、目前專案中的頁面,用的是哪一種?

我請 Codex 只讀、不改,盤點每一頁的資料從哪裡來、畫面在哪裡組成,再從原始碼、正式版的 build、實際的回應三個地方交叉確認:

頁面 資料取得與畫面處理 渲染方式
首頁 伺服器取得最新專輯和藝人、組好頁面;瀏覽器負責互動 SSR
專輯列表(含搜尋) 伺服器載入整份目錄;瀏覽器過濾手上的資料 SSR
專輯詳情 伺服器取得專輯資料;瀏覽器負責版本切換和收藏 build 標 SSG,實際接近 SSR(見 2.1)
我的收藏 伺服器載入專輯目錄;瀏覽器自己去拿收藏清單 SSR + CSR
登入頁 伺服器判斷是否已登入;瀏覽器執行登入流程 SSR
帳號頁 伺服器確認身分後組好頁面;瀏覽器提供登出按鈕,登出由伺服器執行 SSR

build 輸出的 ƒ(Dynamic)代表有人請求時才由伺服器產生,也就是 SSR;● 代表 SSG。

不過 build 只看得出整頁 HTML 在哪裡產生。頁面送到瀏覽器之後,瀏覽器會不會自己再去抓資料,要看程式。判斷是不是 CSR 的關鍵是:頁面上的內容,是不是瀏覽器自己拿資料、再組出來的。只是在瀏覽器裡點按鈕、過濾手上已有的資料,不算 CSR。照這個標準,專案中目前只有收藏清單用到 CSR。

每一頁都是伺服器和瀏覽器分工,但分工的方式不一樣。

2.1 build 時說是 SSG,但回應卻說不能快取

專輯詳情頁「大家看到的都一樣、很少改」,build 也確實把它標成 ● SSG,列出了 25 張專輯的網址。看到 ●,很容易以為使用者拿到的就是事先做好的頁面。

但如果要確認,就得看實際的回應。這裡有個特別的地方:目前實作的專輯詳情有兩種呈現,從列表點進去會開側邊欄,直接打開網址才是完整的詳情頁。兩者網址相同,背後卻是不同的路由,側邊欄在 build 裡本來就是 ƒ。

所以要檢查詳情頁,得把網址貼到新分頁,在 Network 裡找類型是 document 的那個請求,看 Response Headers:

Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate

意思是:這份回應不能被快取,也不能像靜態頁面一樣,重複提供給不同的人。

原因在最外層的版面(根 layout)。為了讓右上角一出現就顯示正確的登入狀態,伺服器要讀使用者的 cookie。build 時確實處理了這 25 個網址,但加上登入狀態之後的完整頁面,每個人都可能不一樣,所以完整回應仍然不能共用。

工具標的分類,不一定等於使用者拿到的結果;看到的請求,也不一定是你以為的那一個。要確認,就看實際的回應。

2.2 要不要改成 ISR?

Codex 也評估了,以目前的架構來說,改成 ISR 要動哪些地方:登入狀態改由瀏覽器判斷、導覽列要多一個「還不確定」的狀態、登出流程要重新設計、收藏要等登入狀態確定才能載入、資料快取策略要另外規劃。另一種做法,是把公開頁面和需要登入的頁面拆成不同的版面。不管哪一種,都不是純粹加一行設定就能完成。

維持現狀 改成 ISR
回應 依每個人的登入狀態組成,不能共用快取 公開內容和個人內容拆開後,可以共用
資料新鮮度 改了馬上看得到 要設定多久更新
右上角登入狀態 一出現就正確 可能要等一下才確定
要改的範圍 無 登入、登出、收藏、快取,或版面拆分

而目前我的決定是先不改,等到資料量變大、需要更快的首頁和分享預覽,或部署後量到第一次載入真的偏慢,再回來處理。


三、搜尋:在哪裡找、什麼時候找

設計搜尋(或是取得/過濾資料)時,其實有兩個要考量的點:在哪裡找(方式),和什麼時候找(時機)。兩者可以自由搭配、組合。接下來這邊,以搜尋/篩選專輯的流程為例子來分享、舉例。

3.1 搜尋的方式

常見的做法大致有三種。差別在於:關鍵字送出去之前,資料會在哪裡?由誰負責找?

A 瀏覽器過濾:打開頁面時,就把全部資料送到瀏覽器;之後打字,瀏覽器直接從手上的資料裡找/過濾。

https://ithelp.ithome.com.tw/upload/images/20261004/20178017lz4gciyugH.png
【圖 6A|瀏覽器過濾】

B 資料庫查詢:瀏覽器手上沒有全部資料。使用者送出搜尋後,才把關鍵字送出去,由資料庫找出符合的資料。

https://ithelp.ithome.com.tw/upload/images/20261004/20178017OVn3ICVA0p.png
【圖 6B|資料庫查詢】

C 搜尋服務:資料事先同步到專門的搜尋引擎,搜尋時由它負責找,並依相關度排序、容忍打錯字。可以用外部的雲端服務(例如 Algolia),也可以自己架設開源的搜尋引擎(例如 Meilisearch),通常不需要從零自己寫。

https://ithelp.ithome.com.tw/upload/images/20261004/20178017cKcxpkd4g3.png
【圖 6C|搜尋服務】

比較適合 優點 限制
A 瀏覽器過濾 資料少、不常變 打字不用等網路 資料越多,第一次載入越重
B 資料庫查詢 資料中到大 只傳需要的資料 每次搜尋都要等網路,要處理分頁
C 搜尋服務 資料大,或需要容錯、相關度排序 打錯字也可能找得到 多一個服務要維護,可能要付費

怎麼選,可以先從資料量和搜尋品質的需求來看:

https://ithelp.ithome.com.tw/upload/images/20261004/20178017nNHtcFNakI.png
【圖 7|選擇搜尋方式的參考流程】

3.2 搜尋的時機

決定在哪裡找之後,還可以決定什麼時候送出搜尋/過濾的請求:

時機 優點 代價 常見搭配
每打一個字 最即時 如果要問伺服器,請求會很多 A(本來就不用問伺服器)
停一下才送出 在即時感和請求數之間取得平衡 要多處理「舊結果比較晚回來」的情況 B、C 的即時搜尋
按 Enter 或搜尋鍵 請求最少,也最好預測 使用者要多按一下 B、C,特別是條件很多的搜尋

https://ithelp.ithome.com.tw/upload/images/20261004/20178017toyHoMymvv.png
【圖 8|選擇搜尋時機的參考流程】

規劃的過程中,通常也需要想好搜尋條件怎麼保存(例如放在網址裡,方便分享和按上一頁),以及一次回傳多少筆資料。

3.3 目前的做法

目前在專案中,AI 撰寫的方式使用的是「A 瀏覽器過濾」加上「每打一個字母就過濾一次」。目前只有 25 張專輯,打開列表頁時,伺服器就把整份目錄交給瀏覽器,後續使用者打字時,瀏覽器直接在手上的資料裡篩選。以這個規模,這樣的組合算是合理的。但是未來資料量假設越來越多,這種情況可能就會造成使用者體驗下降。

AI 會這樣實作,有許多可能的因素。一方面我們從來沒有要求過,另一方面,他也不知道資料量到底有多多。

3.4 資料變多時

前面有提到瀏覽器過濾適合「現在」的專案,但並不代表永遠適合。資料變多時,會遇到三個問題:

問題 原因
第一次載入越來越重 目前列表頁會連同每張專輯的版本、款式、內容物一起送到瀏覽器,但搜尋用不到這些
資料可能默默被截斷 Supabase 的 Data API 預設一次最多回傳 1000 筆;沒有分頁的話,超過的專輯不會出現,畫面也不會提示
篩選會變慢 每打一個字,都要掃過全部資料

通常會分兩步調整,可以不用一次就直接改成資料庫搜尋:

第一步:只送搜尋需要的欄位(標題、藝人名稱),不送版本和內容物 → 一樣在瀏覽器過濾,但輕很多

第二步:資料多到連精簡版都太重時,列表改成分頁,搜尋改成 B 資料庫查詢 → 時機改成停一下或按 Enter 才送出、只回傳前 N 筆、加上適合文字搜尋的索引

實際上,很多網站會混合搭配:瀏覽列表時分頁,一次只拿一部分資料;搜尋時,則把關鍵字送到資料庫,從全部資料裡找。也有網站只把標題、名稱這類輕量資料送到瀏覽器,用來即時顯示搜尋建議,按下 Enter 後再到資料庫查完整結果。

要注意的是,分頁之後可能就不適合再只靠瀏覽器過濾。瀏覽器手上只有目前這一頁,搜尋結果會不完整,而且畫面不會提示少了什麼。

什麼時候該換,沒有固定的筆數標準。看的是第一次載入的資料量,以及使用者有沒有感覺到慢。


四、資料的四種狀態,加上一個操作

只要畫面要等資料,就不會只有「成功」一種結果。

狀態 使用者應該看到 目前專案的實作
載入中 骨架畫面,知道網站正在處理 收藏頁已有、列表頁還沒有
成功 結果 ✓
空的 「找不到」,加上下一步建議 搜尋已有,並提供重設按鈕
錯誤 發生了什麼事 還沒有,之後補上
重試(操作) 一個可以再試一次的按鈕,按下後回到載入中 還沒有,之後補上

等待時,一定要用骨架畫面嗎?

不一定,常見的有三種:

  • 骨架畫面(Skeleton):已經知道內容大概長什麼樣子時,例如專輯卡片列表
  • 載入動畫(Spinner):不容易預先畫出內容輪廓時,例如按下送出後
  • 進度條(Progress Bar):真的知道進度時,例如上傳檔案

重點不是哪一種比較好看,而是讓使用者知道:網站還在處理,還是真的出問題了。不知道實際進度時,就不要做一條跑到 90% 就停住的進度條。

Next.js 對其中兩種提供了固定的做法:

  • loading.tsx:頁面還沒準備好時,先顯示這個畫面
  • error.tsx:捕捉這個頁面沒處理到的錯誤,顯示這個畫面,並可以提供重試按鈕(外部服務還沒恢復的話,重試仍可能失敗)

原本列表頁如果連不上資料庫,使用者只會看到 Next.js 預設的錯誤頁。現在至少會知道發生什麼事,也能自己再試一次。

設計一個需要讀取資料的畫面時,可以先列出這四種狀態和重試的方式,而不是只設計成功的那一種。


五、第一次進入,為什麼可能比較慢?

就算平常很快,第一次進入網站時也可能比較慢,常見的原因有三個:

原因 意思
冷啟動 某些雲端服務閒置一段時間後,第一個請求要多等它重新啟動
第一次下載 瀏覽器第一次要下載網站的程式和圖片,之後才有快取
沒有快取 頁面或資料沒有事先準備好,每次都要重新查

本機開發和正式部署的環境不一樣,這些要等部署之後,在真實環境量過才知道。重點是:慢的時候先量,找出時間花在哪裡,再決定怎麼改,不要先猜,也不要直接叫 AI「幫我優化」。


換個領域:每一頁,都在選渲染方式和資料搜尋/過濾方式

產品 頁面 常見的渲染方式 常見的搜尋/過濾方式
新聞網站 首頁、文章頁 ISR:大家看都一樣,每幾分鐘更新 文章很多,通常交給資料庫或搜尋服務,按 Enter 送出
電商 商品列表、商品頁 ISR;價格、庫存另外即時查 列表分頁;搜尋常交給搜尋服務,停一下就顯示建議
電商 購物車、訂單 SSR 或 CSR:每個人不一樣 自己的訂單通常不多,可以在瀏覽器過濾;很多時改由伺服器查詢
線上文件 文件內容 SSG:很少變 事先整理成精簡索引,在瀏覽器搜尋,每打一個字就更新
公司後台 資料表、報表 CSR 或 SSR:要登入,不需要被搜尋到 資料多,交給資料庫查詢並分頁,設定好篩選條件後才送出

同一個網站,不同頁面可以用不同的方式。選渲染方式時,可以先問:每個人看到的是否一樣?資料多久會變?需要被搜尋到嗎?選搜尋方式時,則看資料有多少、需不需要容錯,以及使用者期待多即時的結果


結語與明日預告

今天沒改動什麼程式碼,但終於看清楚了:一個畫面背後,資料在哪裡、在什麼時候被組成。

快,不只是縮短等待。等待時看到什麼、失敗時能做什麼,也是體驗的一部分。而有些看不見的浪費,可以透過打開DevTools 的 Network 頁籤來進行排查、分析。

到目前為止,網站已經能讓訪客回報錯誤、讓使用者收藏專輯。但回報送進來之後,我只能從 Telegram 一則一則看。哪些處理過了?哪些還沒?

一般使用者只能碰自己的收藏;管理者卻需要看別人送進來的回報。那網站怎麼分辨,誰真的有管理權限?

明天,我們來實作回報管理後台:一個只有管理者看得到的地方。

我們明天見。


上一篇
【Day 19|食人島】收藏功能實作與 RLS:登入了,就能看別人的資料嗎?
下一篇
【Day 21|瞭望】管理員後台與角色權限:登入只是第一關
系列文
《我與 AI 的奇幻漂流:30 天,把「能跑」變成「能上線」》 共 22 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言