← 所有文章

工程 · 2026.10.25 · 4 min · 更新於 2026.10.26

發文要等兩分鐘:把 Astro 靜態部落格改成 Workers 即時渲染

部落格原本每次發文都要全站重 build,等 1 到 2 分鐘。改成 Cloudflare Workers 即時渲染加邊緣快取後,發文只清一次快取,約 5 秒上線,CPU 中位數 9ms,還留在免費方案。

閱讀設定
主題
字級
寬度

按下發佈之後,這個 Astro 靜態部落格要全站重 build,1 到 2 分鐘後讀者才看得到新文章。

我把它改成在 Cloudflare Workers 上即時渲染、結果存進邊緣快取。發文時後端只要清一次快取,大約 5 秒就上線。

線上量到的渲染 CPU 中位數 9ms,命中快取只要 1 到 3ms,目前還在免費方案。

過程中最容易漏的坑是:Worker 存進 Cache API 的回應,會被 zone 的瀏覽器快取 TTL 改寫。

後端照舊當 CMS

前台是 Astro,放在 Cloudflare Pages;文章存在自架的 Spring Boot 加 PostgreSQL 後端,從自己的後台發佈。

發文時後端觸發 GitHub Actions 全站重 build,文章越多越慢。我想要的是像 WordPress 那樣,按下發佈就上線。

最後選的是讓 Worker 即時渲染頁面,後端繼續當 CMS。

另外兩條路都要重寫一大塊。讓後端直接產 HTML,等於整個前台重寫;把資料全部搬到 D1 和 R2(Cloudflare 的資料庫和檔案儲存),登入、2FA、留言、推播都要重寫。

用 KV(Cloudflare 的鍵值儲存)存渲染結果也想過,但 KV 全球同步要一分鐘左右,發文沒辦法馬上生效,所以排到之後。

流程對照:以前後台發佈後要觸發 GitHub Actions 全站重 build 再部署到 Pages,等 1 到 2 分鐘;現在後端等 3 秒合併連續操作,清掉 Cache-Tag html,下一位讀者進來時 Worker 即時渲染並存進快取,約 5 秒上線。

先用 POC 量 CPU 和網路

切換前先在 workers.dev 和一個臨時子網域上做 POC,量完就刪。

Cache API 在 workers.dev 網域上不會生效,put 等於沒做,測快取得掛自己的網域。

300 多次渲染沒有失敗,渲染一頁的 CPU 時間如下:

情況 渲染 CPU 時間
命中快取 1 到 3ms
本機熱身後 約 3ms
線上中位數 9ms
線上 p90 10ms
線上範圍 4 到 46ms

意外的是網路。從台灣連 Cloudflare 會被繞到 SJC(聖荷西),Worker 每打一次後端約 350ms。

所以文章頁的清單和內文改成同時抓,不要一個等一個。

快取 key 與 Cache-Tag

Astro 加上 Cloudflare adapter,首頁、文章、tag、封存、RSS、sitemap、404 改成在 Worker 即時渲染,關於頁和後台維持靜態。

渲染結果由 middleware 存進 Cache API,有三個細節:

  • 快取 key 帶 build ID,部署新版就自動換一批快取,舊 HTML 不會指到已經刪掉的 CSS。
  • key 去掉 query string,亂帶參數打不穿快取。
  • 存進去的回應加上 Cache-Tag: html,lang:<語系>,讓後端用 tag 清。

Cache API 是每個資料中心各自一份,清快取則是全球生效。

用網址或用 tag 清快取,對 Cache API 都有效,只清到指定的頁面。免費方案也能用 tag 清。

後端在發佈、更新、刪除已發佈的文章,或排程文章到點時,清掉 tag html。它會等最後一次觸發後 3 秒才送,連續操作合併成一次。

被改寫的 Cache-Control

Worker 用 caches.default.put() 存進去的回應,之後 match() 拿出來時,Cache-Control 已經不是當初存的值。

zone 的瀏覽器快取 TTL 預設 4 小時,會把它改成 max-age=14400。

直接回給讀者的話,瀏覽器會留著舊頁 4 小時,後端清了邊緣快取也沒用。

改法是把邊緣和瀏覽器分開管。存進快取的那份用 s-maxage(只給 CDN 這類共用快取看的存活時間)控制邊緣存多久。

不管命中或沒命中,送出前都把給瀏覽器的 header 蓋回去,讓瀏覽器每次回來問邊緣。存的那份用 clone(),兩邊互不影響。

TypeScript
res.headers.set('Cache-Control', 'public, s-maxage=604800') // 給邊緣,數值是例子
await caches.default.put(key, res.clone())
res.headers.set('Cache-Control', 'public, max-age=0, must-revalidate') // 給瀏覽器

示意圖:Worker 渲染出的回應分成兩份。存進 Cache API 的那份帶 s-maxage 和 Cache-Tag,key 是 build ID 加去掉 query 的路徑,取出時會被 zone 瀏覽器 TTL 改成 max-age=14400;送給瀏覽器的那份不管命中與否,一律蓋成 max-age=0, must-revalidate。

切換結果與還沒解的問題

切換時 Worker 用路由攔在 Pages 前面,Pages 留著當退路,刪掉路由就退回原站。

正式站實測清快取,狀態照預期從 HIT 變 MISS、再回到 HIT。

有幾個舊問題跟著解掉。

之前刪掉的兩篇文章,在 Cloudflare 清了兩次快取都還在。查下去是 Pages 原站自己回舊版本,帶 noindex、快取一週,清 zone 快取碰不到;切到 Worker 之後直接 404。

RSS 原本讀 repo 裡的舊 markdown,訂閱的人看不到新文章,這次改讀後台。

搜尋原本用 Pagefind,索引要 build 時產生,改成打後端的搜尋 API。

封面改走 Cloudflare 的圖片轉換,從同網域輸出,首頁 WebGL 貼圖不會有 CORS 問題。

Worker 裡推算遠端圖片寬高會失敗,所以改成後端上傳時讀檔頭記下寬高,舊的 252 張圖補算,74 篇內文重新 render。

還沒解的:清快取在最後一次觸發後 3 秒才送,這 3 秒內後端剛好重啟,就會漏清一次。

CPU 也還在觀察,常超標就先加 KV 存渲染結果,再不行才升級付費方案。

這次最大的收穫是,靜態站發文慢不一定要換掉後端,讓邊緣即時渲染、後端照舊當 CMS,改動範圍最小。

#Cache API#Astro#Cloudflare Workers#邊緣快取

留言

載入留言中…

下一篇

投資

回測結果可信嗎?TradingView 回撤低估、偷看未來等五個回測陷阱

回測跑出漂亮數字,不代表策略真的有用。整理我做策略研究時踩過的坑:TradingView 最大回撤低估、沒有隨機對照組、資料沒洗、樣本太短、偷看未來,以及各自怎麼避開。