How GeoLibre Implements PWA Offline Support with Workbox Service Workers
GeoLibre uses a three-part architecture combining the Vite-PWA plugin's Workbox integration, manual service worker registration, and warm-up fetch logic to enable complete offline map functionality after the first online visit.
The opengeos/GeoLibre repository demonstrates a production-ready approach to PWA offline support with Workbox service workers in a complex geospatial application. This article breaks down the exact implementation patterns, configuration decisions, and code paths that make offline vector and raster mapping possible.
Overview: Three Cooperating Components
GeoLibre's offline capability depends on three systems working together:
- Build-time Workbox configuration — defines precache and runtime caching rules
- Manual service worker registration — gives the app control over SW lifecycle
- Runtime warm-up fetches — populates caches through the SW rather than bypassing it
This design keeps initial page loads fast while ensuring heavy geospatial engines and basemap tiles become available for subsequent offline use.
Vite-PWA Plugin and Workbox Configuration
The foundation sits in vite.config.ts, where the pwaPlugin() function configures Workbox behavior (lines 81–111). Key decisions in this configuration include:
injectRegister: false— disables automatic SW injection so the app can register manually- Precache patterns via
globPatterns— captures JS, CSS, HTML, and font assets needed for the app shell - Deliberate exclusions via
globIgnores— leaves out large lazy-loaded chunks (MapLibre, DuckDB-WASM, Cesium) to reduce first-visit bundle size
The runtimeCaching section establishes Cache-First strategies for three asset categories:
| Asset Type | Strategy | Example Sources |
|---|---|---|
| Same-origin hashed assets | CacheFirst | Build chunks with content hashes |
| CDN-loaded geospatial engines | CacheFirst | Pyodide, PGlite, CereusDB via jsdelivr |
| Basemap tiles | CacheFirst | geolibre-basemaps and other allowed tile hosts |
Version-pinned CDN URLs ensure cache correctness: when PGLITE_CDN_URL or similar constants update with new package versions, the URL changes and Workbox fetches fresh rather than serving stale engines.
Manual Service Worker Registration
With injectRegister: false, GeoLibre takes control of when and how the service worker activates. The registration code typically lives in src/main.tsx, coordinated with the stale-chunk-reload logic from src/lib/stale-chunk-reload.ts:
// src/main.tsx — simplified manual registration
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js', { scope: '/' })
.then(() => console.info('[GeoLibre] SW registered'))
.catch(err => console.error('[GeoLibre] SW registration failed', err));
}
This manual approach lets the application:
- Share lifecycle state with the stale-chunk reload coordinator
- Control timing relative to other initialization steps
- Handle registration failures with custom UI feedback
Warm-Up Logic: Driving the Cache Through the SW
The src/lib/offline-tiles.ts module implements the user-facing offline region download feature. Rather than writing directly to Cache Storage—which would bypass Workbox's routing and metadata—the module uses standard fetch() requests that flow through the active service worker.
URL Collection and Deduplication
The collectOfflineUrls() function (lines 404–417) gathers every URL needed for a selected region:
// Resolving URLs for an offline download
const { urls, tileUrls } = await collectOfflineUrls(
map,
bbox, // [-122.5, 37.7, -122.3, 37.8]
minZoom, // 10
maxZoom // 14
);
Supporting functions handle source-specific logic:
resolveTileSources()— fetches TileJSON when sources provide only aurlfieldclampZoomRange()— prevents over-zoom requests that would 404collectTileUrls()andcollectStyleAssetUrls()— enumerate tiles, sprites, and glyphs
All URLs are deduplicated so the preview size matches actual download requirements.
Bounded Concurrent Fetching
The warmUrls() function (lines 443–514) executes the download with controlled concurrency:
import { warmUrls } from '@/lib/offline-tiles';
const progress = await warmUrls(urls, {
concurrency: 8, // parallel fetches
timeoutMs: 30_000, // per-request timeout
onProgress: p => console.log(`Progress: ${p.done}/${p.total}`, p)
});
// progress.failedUrls contains any URLs that need retry
Each fetch passes through the active SW, matching Workbox's CacheFirst routes and populating the corresponding cache entries. The bounded concurrency prevents overwhelming the browser or tile servers.
Service Worker Presence Check
Before starting downloads, the UI verifies SW readiness using hasActiveServiceWorker() (lines 16–24):
import { hasActiveServiceWorker } from '@/lib/offline-tiles';
if (!hasActiveServiceWorker()) {
alert('Offline download requires the service worker to be active. Reload the page and try again.');
}
This guard returns true only when navigator.serviceWorker.controller exists, ensuring cache population won't silently fail.
Step-by-Step Offline Flow
| Step | Action | Implementation Location |
|---|---|---|
| 1 | Vite build runs pwaPlugin(), Workbox generates sw.js with precache manifest and runtime routes |
vite.config.ts lines 81–111 |
| 2 | App boots and manually registers /sw.js |
src/main.tsx |
| 3 | User selects offline region; collectOfflineUrls() resolves all tile, sprite, and glyph URLs |
offline-tiles.ts lines 404–417 |
| 4 | warmUrls() fetches URLs; SW intercepts each request and applies CacheFirst caching |
offline-tiles.ts lines 443–514 |
| 5 | Subsequent launches (including offline) retrieve assets from SW caches | Generated sw.js |
| 6 | Failed tiles reported in progress.failedUrls for UI retry handling |
warmUrls() return value |
Design Decisions and Trade-offs
Cache-First for runtime assets prioritizes offline availability over freshness. For geospatial engines with version-pinned URLs, this is safe—the URL content never changes. For basemap tiles, users accept potentially stale tiles in exchange for offline functionality.
Excluding heavy chunks from precache keeps Time-to-Interactive low on first visit. MapLibre, DuckDB-WASM, and Cesium load on-demand and cache thereafter, making them available offline without blocking initial render.
Graceful degradation in warmUrls() counts failures rather than aborting. A partially warmed region remains usable, and the UI can offer retry functionality for specific failed tiles.
Summary
- Workbox configuration in
vite.config.tsseparates app shell precaching from lazy runtime caching of heavy engines and tiles - Manual registration in
main.tsxcoordinates the service worker lifecycle with app initialization and stale-chunk handling - Warm-up fetches through
offline-tiles.tspopulate caches by flowing requests through the SW, respecting Workbox routing rules - Bounded concurrency and timeout handling prevent overwhelming network resources during region downloads
- Version-pinned CDN URLs eliminate cache invalidation problems for external geospatial engines
Frequently Asked Questions
What prevents stale geospatial engines from being served offline?
GeoLibre uses version-pinned CDN URLs with exact package versions embedded in the path (e.g., PGLITE_CDN_URL). When dependencies upgrade, the URL changes and Workbox treats it as a new resource, fetching the updated version rather than serving a stale cache entry.
Why does GeoLibre disable automatic service worker injection?
The injectRegister: false setting allows the application to control registration timing and share lifecycle state with the stale-chunk-reload logic in src/lib/stale-chunk-reload.ts. This coordination ensures predictable behavior when lazily-loaded chunks 404 after a new deployment.
How does the offline region download handle partial failures?
The warmUrls() function in offline-tiles.ts counts failed fetches without aborting the entire operation. It returns a failedUrls array in the progress object, enabling the UI to show which tiles failed and offer a retry button for those specific URLs.
Can the service worker cache tiles from any basemap provider?
No—Workbox runtimeCaching routes in vite.config.ts explicitly whitelist allowed tile hosts. The geolibre-basemaps source and other configured origins match the CacheFirst strategy; requests to unconfigured hosts bypass the SW and fail when offline.
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 →