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:

  1. Precaching – Core app shell (HTML, JS, CSS, fonts)
  2. Runtime caching – Assets – Large hashed chunks from /assets/
  3. 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 output
  • globIgnores – Excludes heavy, lazily-loaded chunks (DuckDB-WASM, Pyodide, etc.) that would exceed the size limit
  • maximumFileSizeToCacheInBytes – 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 completion
  • onNeedReload as no-op – Keeps the current page alive; map state is preserved
  • clientsClaim: true and skipWaiting: true in 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 globPatterns with a 4 MB size limit and explicit ignores for heavy chunks
  • Runtime caching uses CacheFirst strategy for three categories: local assets (geolibre-assets), CDN engines (geolibre-cdn-engines), and basemap tiles (geolibre-basemaps)
  • Registration in src/main.tsx opts for immediate activation with a no-op reload handler to preserve application state
  • Extension requires adding URL patterns to the runtimeCaching array or adjusting globPatterns and 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →