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.