How to Configure PWA Offline Caching with Workbox in GeoLibre
GeoLibre uses Vite's VitePWA plugin with Workbox underneath to generate a service worker that precaches the app shell and implements runtime caching for heavy assets, CDN engines, and basemap tiles.
GeoLibre is an open-source geospatial desktop application built with React and Vite. Its Progressive Web App (PWA) capabilities rely on Workbox for reliable offline caching of critical resources. This guide breaks down the exact configuration found in the opengeos/GeoLibre repository so you can implement, customize, or extend the offline caching strategy.
Understanding the Workbox Architecture in GeoLibre
The PWA configuration lives in apps/geolibre-desktop/vite.config.ts. Vite's VitePWA plugin invokes Workbox's generateSW at build time, producing a service worker that handles three distinct caching layers:
- Precaching – Core app shell (HTML, JS, CSS, fonts)
- Runtime caching – Assets – Large hashed chunks from
/assets/ - Runtime caching – External resources – CDN-hosted engines and basemap tiles
Precaching the App Shell
The globPatterns setting tells Workbox which files to include in the precache manifest generated at build time.
// vite.config.ts – workbox configuration
precaching: {
globPatterns: ["**/*.{js,css,html,woff,woff2}"],
globIgnores: HEAVY_PRECACHE_IGNORES,
maximumFileSizeToCacheInBytes: 4 * 1024 * 1024, // 4 MB
}
globPatterns– Matches all critical static assets in the build outputglobIgnores– Excludes heavy, lazily-loaded chunks (DuckDB-WASM, Pyodide, etc.) that would exceed the size limitmaximumFileSizeToCacheInBytes– Prevents bloating the precache with files over 4 MB
Files matching these patterns are hashed and versioned; Workbox serves them cache-first and updates them when the build hash changes.
Runtime Caching for Large Assets
Assets too heavy for precaching are cached on first fetch using the CacheFirst strategy.
// vite.config.ts – runtimeCaching array
{
urlPattern: ({ url }: { url: URL }) =>
url.pathname.startsWith("/assets/") &&
/\.(js|css|wasm|data|woff2?)$/.test(url.pathname),
handler: "CacheFirst",
options: {
cacheName: "geolibre-assets",
expiration: { maxEntries: 200, maxAgeSeconds: 60 * 60 * 24 * 60 }, // 60 days
cacheableResponse: { statuses: [0, 200] },
},
}
This pattern catches:
- DuckDB-WASM binaries
- MapLibre GL JS bundles
- Font files and data chunks
The geolibre-assets cache stores up to 200 entries for 60 days with opaque-response support (statuses: [0, 200]).
Runtime Caching for CDN Engines
GeoLibre loads computational engines from cdn.jsdelivr.net. These are cached aggressively so the app works offline after first load.
// vite.config.ts – runtimeCaching for CDN engines
{
urlPattern: ({ url }: { url: URL }) =>
url.hostname === "cdn.jsdelivr.net" &&
(
url.pathname.includes("pyodide") ||
url.pathname.includes("pglite") ||
url.pathname.includes("cereus") ||
url.pathname.includes("gdal")
),
handler: "CacheFirst",
options: {
cacheName: "geolibre-cdn-engines",
expiration: { maxEntries: 100, maxAgeSeconds: 60 * 60 * 24 * 90 }, // 90 days
},
}
The geolibre-cdn-engines cache holds Pyodide, PGlite, Cereus, and GDAL WebAssembly builds—totaling tens of megabytes but essential for offline geoprocessing.
Runtime Caching for Basemap Tiles
Offline map viewing requires caching raster tiles from CORS-friendly tile servers.
// vite.config.ts – runtimeCaching for basemaps
{
urlPattern: ({ url }: { url: URL }) => {
const hosts = ["tiles.openfreemap.org", "basemaps.cartocdn.com"];
return hosts.includes(url.hostname) && /\/\d+\/\d+\/\d+\.p?\w+$/.test(url.pathname);
},
handler: "CacheFirst",
options: {
cacheName: "geolibre-basemaps",
expiration: { maxEntries: 8000, maxAgeSeconds: 60 * 60 * 24 * 30 }, // 30 days
cacheableResponse: { statuses: [0, 200] },
},
}
The generous maxEntries: 8000 supports the "Download Offline Area" feature implemented in src/lib/offline-tiles.ts, which proactively fetches tile pyramids for user-defined regions.
Service Worker Registration and Activation
In apps/geolibre-desktop/src/main.tsx, the service worker is registered manually to integrate with stale-chunk reload logic:
// src/main.tsx – service worker registration
import { registerSW } from "virtual:pwa-register";
registerSW({
immediate: true,
onNeedReload() {
// No-op: prevents full-page reload on SW update
// Stale chunks trigger separate reload via installStaleChunkReload
},
onRegisterError(error) {
console.error("[GeoLibre] Service worker registration failed", error);
},
});
Key behaviors:
immediate: true– Registers the SW without waiting for page load completiononNeedReloadas no-op – Keeps the current page alive; map state is preservedclientsClaim: trueandskipWaiting: truein the generated SW – New SW activates immediately
The installStaleChunkReload utility (referenced in the same file) handles lazy-loaded chunks that may mismatch after deployment, requesting a controlled reload only when necessary.
Customizing the Caching Configuration
Add a New Tile Source
Append a runtime caching rule matching your tile server hostname and path pattern:
// vite.config.ts – add to workbox.runtimeCaching
{
urlPattern: ({ url }: { url: URL }) =>
url.hostname === "mytileserver.example.com" &&
url.pathname.match(/^\/tiles\/\d+\/\d+\/\d+\.png$/),
handler: "CacheFirst",
options: {
cacheName: "geolibre-custom-tiles",
expiration: { maxEntries: 5000, maxAgeSeconds: 60 * 60 * 24 * 14 },
},
}
Increase Precache Size Limit
For larger app shells, raise the byte limit:
// vite.config.ts
workbox: {
maximumFileSizeToCacheInBytes: 6 * 1024 * 1024, // 6 MB
}
Cache a New CDN Domain
Mirror the CDN engine pattern for additional hosts:
// vite.config.ts – runtimeCaching entry
{
urlPattern: ({ url }: { url: URL }) =>
url.hostname === "unpkg.com" && url.pathname.includes("my-library"),
handler: "CacheFirst",
options: {
cacheName: "geolibre-unpkg",
expiration: { maxEntries: 50, maxAgeSeconds: 60 * 60 * 24 * 30 },
},
}
Workbox Options Reference
The complete Workbox configuration in vite.config.ts sets these additional options:
| Option | Setting | Purpose |
|---|---|---|
clientsClaim |
true |
New SW takes control of pages immediately |
skipWaiting |
true |
Bypass waiting phase, activate instantly |
cleanupOutdatedCaches |
true |
Remove old precached files on update |
sourcemap |
true |
Debug service worker in DevTools |
These ensure users always receive the freshest cached content without manual intervention.
Summary
- Precaching captures the app shell via
globPatternswith a 4 MB size limit and explicit ignores for heavy chunks - Runtime caching uses
CacheFirststrategy for three categories: local assets (geolibre-assets), CDN engines (geolibre-cdn-engines), and basemap tiles (geolibre-basemaps) - Registration in
src/main.tsxopts for immediate activation with a no-op reload handler to preserve application state - Extension requires adding URL patterns to the
runtimeCachingarray or adjustingglobPatternsand size limits
Frequently Asked Questions
What is the maximum file size GeoLibre precaches by default?
GeoLibre sets maximumFileSizeToCacheInBytes to 4 MB (4,194,304 bytes). Files exceeding this limit—such as DuckDB-WASM or Pyodide bundles—are excluded from precaching and handled via runtime caching instead. You can raise this value in vite.config.ts if your build produces larger critical assets.
How does GeoLibre prevent page reloads when the service worker updates?
The registerSW call in src/main.tsx passes an empty onNeedReload callback. This disables the default VitePWA behavior of reloading the page when a new service worker activates. Instead, clientsClaim: true and skipWaiting: true let the new SW take control immediately. Stale lazy-loaded chunks are handled separately by installStaleChunkReload.
Where is the "Download Offline Area" feature implemented?
The tile prefetching logic lives in apps/geolibre-desktop/src/lib/offline-tiles.ts. It calculates tile URLs for a geographic bounding box and zoom range, then fetches them into the geolibre-basemaps cache. This warms the runtime cache so tiles are available offline without waiting for normal map navigation.
Can I use a different caching strategy than CacheFirst?
Yes—replace handler: "CacheFirst" with any Workbox strategy: NetworkFirst, StaleWhileRevalidate, NetworkOnly, or CacheOnly. For basemap tiles that update infrequently, CacheFirst is optimal. For live data overlays, NetworkFirst or StaleWhileRevalidate may be more appropriate.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →