開發歷程:讓工具離線也能用——自托管與 Service Worker 分層快取

這是 LocalPapa Notes 開發紀錄系列的第九篇。前面講了很多「工具能做什麼」;這一篇講一件你看不到、卻每次造訪都在受益的底層工程——離線化(PWA)與 Service Worker 快取。也就是這個部落格每篇結尾都會提到、我每次發版都在更新的那個 sw.js

目標:第二次造訪該是「瞬開」的

LocalPapa 是一堆瀏覽器工具。理想體驗是:第一次打開正常載入,第二次造訪幾乎瞬間開啟,甚至斷網也能用

要做到這件事,得先面對一個現實——我們依賴不少外部 CDN 函式庫。每次造訪都去外站抓這些檔案,既慢、又把可用性綁在第三方身上:CDN 一抖,你的工具就掛。

盤點之後的取捨:不是「全部自托管」

直覺解法是「把所有外部依賴都搬回自己 repo」。但盤點之後,這條路不務實。

我們發現依賴分兩類:

  • FontAwesome:被 30 個頁面共用——槓桿最高的一個。
  • 各工具專屬的重型函式庫:ffmpeg 的 wasm 約 30MB、還有 mermaid、pdf-lib、d3、chart.js……每個都很大,而且只有特定工具會用到。

把幾十個函式庫加上 30MB 的二進位全部 commit 進 repo?那會讓 repo 暴肥、clone 變慢、Pages 流量上升。所以不該一刀切,而是分兩層處理。

第一層:把 FontAwesome 自托管

FontAwesome 是最值得搬回來的——30 頁共用、體積不大、又是每頁首屏就要的字型圖示。

我們從 npm 取得 all.min.css 與 woff2 字型,放進 /vendor/fontawesome/。這裡有個小陷阱:FontAwesome 的 CSS 用相對路徑 ../webfonts/ 去找字型檔,所以 css/webfonts/ 必須放成同級目錄才解析得到。接著把全站 30 處引用批次改成本地相對路徑(首頁用 vendor/...、工具頁用 ../vendor/...)。

第二層:重型函式庫交給 Service Worker 快取

那 30MB 的 ffmpeg、那些只有單一工具會用的大塊頭呢?不自托管,改讓瀏覽器在「第一次用過之後」自己快取起來。

這就是 sw.js 的工作。它的核心是一套分層快取策略,依請求類型分流:

  • 導覽的 HTML 頁面 → network-first(線上優先):永遠先抓最新的頁面,離線時才退回快取。這點很關鍵——否則你部署了新版,使用者卻一直卡在快取的舊頁。
  • 同源靜態資源(CSS/JS/圖示)→ stale-while-revalidate:先用快取秒回,同時背景默默抓最新版更新快取。下次就是新的。
  • 允許清單上的 CDN 函式庫 → cache-first:jsDelivr/cdnjs/unpkg 這些版本化、內容不可變的網址,命中快取就直接回,不再往外跑。(它們都回傳 CORS 標頭,所以能正常 cache.put。)

效果是:那 30MB 的 wasm,你用過一次之後就被瀏覽器快取承載,第二次(甚至離線)都不必再下載——我們既得到離線能力,又沒把大檔塞進 repo。

版本號藏在快取名稱裡

你可能注意到 sw.js 開頭有一行 const VERSION = 'lp-vX.Y.Z'。快取的名稱就內嵌這個版本號;當 Service Worker activate 時,會自動清掉所有非當前版本的舊快取

這就是為什麼每次發版我都會動它一下——改版本號,就是給所有使用者一次乾淨的快取更新。(也是為什麼前面修部落格 bug 時,我老提醒你「強制刷新」拿新的 SW。)

註冊一次,控制全站

Service Worker 放在根目錄 /sw.js,控制範圍就是整個網站。我們在首頁與所有工具頁共用的 tools-common.js 裡,用 load 事件註冊它——一處註冊,全站受益。再加上 manifest.webmanifesttheme-color,整個站就能被「安裝成 App(PWA)」放到桌面或主畫面。

學到的事

  • 自托管不是非黑即白,是看槓桿。 30 頁共用的小檔值得搬回來;30MB 的單一工具大檔,交給瀏覽器快取更聰明。
  • 快取策略要分請求類型。 HTML、同源資源、第三方 CDN 各有最適策略;一招套全站,不是太舊就是太慢。
  • HTML 一定要 network-first。 這是「部署後使用者看不到新版」這個經典坑的解法。
  • 版本化快取名稱讓更新可控。 一個版本字串,就能在 activate 時乾淨汰換舊快取。
  • 離線能力是 local-first 的自然延伸。 既然工具本來就在你瀏覽器裡跑、檔案不上傳,那連網路都不需要,才是這個理念的完整樣貌。

一以貫之的問題

這一篇把系列的核心又補上一塊:不只是「能不能在瀏覽器完成」,而是「連網路斷了,還能不能用?」

對 LocalPapa 來說,答案趨近於:可以。工具在你的裝置裡,現在連快取也是。

下一篇開發紀錄,我們會走進一條全新的長線——把整個網站變成 AI agent 讀得懂的知識包(OKF)與 llms.txt。想追蹤系列?把 LocalPapa Notes 加入書籤吧。

Dev Log: Making the Tools Work Offline — Self-Hosting and a Layered Service Worker Cache

This is the ninth post in the LocalPapa Notes dev-log series. We've talked a lot about "what the tools can do"; this one is about something you never see but benefit from on every visit — offline support (PWA) and Service Worker caching. The very sw.js this blog mentions at the end of every post, and that I bump on every release.

The goal: the second visit should be "instant"

LocalPapa is a pile of browser tools. The ideal experience is: the first open loads normally, and the second visit opens almost instantly — even with no network.

To pull that off, you first face a reality: we depend on a fair number of external CDN libraries. Fetching them from elsewhere on every visit is slow and ties availability to a third party: the CDN hiccups, your tool breaks.

The trade-off after the audit: not "self-host everything"

The instinctive fix is "move every external dependency back into our own repo." But after an audit, that path isn't practical.

We found dependencies fall into two classes:

  • FontAwesome: shared by 30 pages — the highest-leverage one.
  • Per-tool heavy libraries: ffmpeg's wasm is about 30MB, plus mermaid, pdf-lib, d3, chart.js… each large, and each used only by a specific tool.

Commit dozens of libraries plus a 30MB binary into the repo? That bloats the repo, slows clones, and raises Pages bandwidth. So don't go all-or-nothing — handle it in two layers.

Layer one: self-host FontAwesome

FontAwesome is the one most worth bringing back — shared by 30 pages, not large, and needed in the first paint of every page.

We grabbed all.min.css and woff2 fonts from npm into /vendor/fontawesome/. There's a small trap here: FontAwesome's CSS references fonts with the relative path ../webfonts/, so css/ and webfonts/ must sit as sibling directories for it to resolve. Then we batch-changed all 30 references site-wide to local relative paths (homepage uses vendor/..., tool pages use ../vendor/...).

Layer two: leave heavy libraries to the Service Worker cache

What about that 30MB ffmpeg, and the big chunks only a single tool uses? Don't self-host them — let the browser cache them after the first use.

That's sw.js's job. At its core is a layered caching strategy that routes by request type:

  • Navigation HTML pages → network-first: always fetch the latest page first, falling back to cache only when offline. This is crucial — otherwise you deploy a new version and users stay stuck on a cached old page.
  • Same-origin static assets (CSS/JS/icons) → stale-while-revalidate: serve from cache instantly, while quietly fetching the latest in the background to update the cache. Next time it's fresh.
  • Allowlisted CDN libraries → cache-first: versioned, immutable URLs like jsDelivr / cdnjs / unpkg return straight from cache on a hit, no outbound trip. (They return CORS headers, so cache.put works.)

The effect: that 30MB wasm is carried by the browser cache after you use it once, and the second time (even offline) it isn't downloaded again — we get offline capability and keep the big file out of the repo.

The version number hides in the cache name

You may have noticed sw.js opens with a line const VERSION = 'lp-vX.Y.Z'. The cache names embed that version; when the Service Worker activates, it automatically purges every cache that isn't the current version.

That's why I nudge it on every release — changing the version number gives every user a clean cache update. (It's also why, when fixing the blog bugs earlier, I kept reminding you to "force refresh" to get the new SW.)

Register once, control the whole site

The Service Worker lives at the root, /sw.js, so its scope is the entire site. We register it via a load event in tools-common.js, shared by the homepage and every tool page — register in one place, benefit everywhere. Add a manifest.webmanifest and theme-color, and the whole site can be "installed as an app (PWA)" onto a desktop or home screen.

What we learned

  • Self-hosting isn't black and white — it's about leverage. A small file shared by 30 pages is worth bringing back; a 30MB single-tool file is smarter left to the browser cache.
  • Cache strategy must vary by request type. HTML, same-origin assets, and third-party CDNs each have an optimal approach; one rule for everything is either too stale or too slow.
  • HTML must be network-first. This is the fix for the classic "users can't see the new version after deploy" pit.
  • Versioned cache names make updates controllable. One version string cleanly retires old caches on activate.
  • Offline capability is a natural extension of local-first. If the tools already run in your browser with no upload, then not even needing the network is the full form of that philosophy.

The same question, completed

This post adds another piece to the series' core: not just "can this be done in the browser," but "if the network drops, does it still work?"

For LocalPapa, the answer trends toward: yes. The tools are on your device — and now the cache is too.

In the next dev log, we step into a whole new thread — turning the entire site into a knowledge pack AI agents can read (OKF) plus llms.txt. Want to follow the series? Bookmark LocalPapa Notes.

探索 39+ 個隱私優先的瀏覽器工具
全程本地運算、檔案不上傳。
前往 LocalPapa →
Explore 39+ privacy-first browser tools
Everything runs locally — your files never leave your device.
Visit LocalPapa →

想看英文版?點右上角 EN 切換語言。

Prefer Chinese? Tap at the top-right to switch.