Service Worker for Map Tiles Caching
This guide explains how to implement Service Worker caching to improve map loading performance and enable offline functionality for the MapVX Web SDK.
Overview
Service Workers allow you to cache map tiles and other assets, significantly improving performance on repeated visits and providing offline capabilities. This is independent of the SDK but greatly enhances its performance by caching map resources.
Benefits
- ✅ Faster Loading: Cached tiles load instantly on repeat visits
- ✅ Offline Support: Maps work without internet connection
- ✅ Reduced Bandwidth: Less data usage for returning users
- ✅ Better User Experience: Smoother map interactions
- ✅ Performance Optimization: Reduced server load
Implementation Approaches
There are different ways to implement Service Workers depending on your project needs:
1. Manual Service Worker (injectManifest)
Write your own Service Worker for maximum flexibility. Best for complex caching requirements or when you need additional Service Worker features like push notifications.
2. Generated Service Worker (generateSW)
Let Workbox generate the Service Worker for you. Best for simple caching needs and quick setup.
3. Build Tool Integration
Use Workbox plugins for bundlers like Webpack, Vite, or Rollup. Best when you want Service Worker generation as part of your build process.
Basic Implementation
1. Service Worker Setup
You need to manually register and serve a Service Worker file from your site’s root directory (HTTPS required). This Service Worker will intercept network requests and cache map resources to improve performance.
2. Basic Service Worker (Workbox)
Create a file named mapvx-tiles-sw.js in your site’s root directory:
/*
MapVX Web SDK Service Worker (Workbox)
- Cache-first strategy for map tiles (PBF files)
- Stale-while-revalidate for other map assets
- Network-first for API calls
Requirements:
1) Save this file as "/mapvx-tiles-sw.js" in your site root
2) Ensure your site is served over HTTPS
3) Register it manually in your application code
*/
/* eslint-env serviceworker */
/* global workbox, importScripts */
importScripts("https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js")
const { registerRoute } = workbox.routing
const { CacheFirst, StaleWhileRevalidate, NetworkFirst } = workbox.strategies
const { CacheableResponsePlugin } = workbox.cacheableResponse
const { ExpirationPlugin } = workbox.expiration
// Preload Workbox modules
workbox.loadModule("workbox-routing")
workbox.loadModule("workbox-strategies")
workbox.loadModule("workbox-cacheable-response")
workbox.loadModule("workbox-expiration")
// Allowed hosts for caching (adjust based on your API endpoints)
const ALLOWED_HOSTS = ["api.mapvx.com", "maps.mapvx.com", "tiles.mapvx.com"]
const isAllowedHost = (hostname) => ALLOWED_HOSTS.some((host) => hostname.includes(host))
// API calls → Network-first strategy
registerRoute(
({ url }) => isAllowedHost(url.hostname) && url.pathname.includes("/api/"),
new NetworkFirst({
cacheName: "mapvx-api-cache",
plugins: [
new CacheableResponsePlugin({
statuses: [0, 200],
}),
new ExpirationPlugin({
maxEntries: 50,
maxAgeSeconds: 60 * 60, // 1 hour
}),
],
})
)
// Map tiles (PBF files) → Cache-first strategy
registerRoute(
({ url }) => isAllowedHost(url.hostname) && /\.pbf($|\?)/.test(url.pathname),
new CacheFirst({
cacheName: "mapvx-tiles-pbf",
plugins: [
new CacheableResponsePlugin({
statuses: [0, 200],
}),
new ExpirationPlugin({
maxEntries: 10000,
maxAgeSeconds: 60 * 60 * 24 * 7, // 1 week
}),
],
})
)
// Map styles and other assets → Stale-while-revalidate
registerRoute(
({ url }) => isAllowedHost(url.hostname) && !/\.pbf($|\?)/.test(url.pathname),
new StaleWhileRevalidate({
cacheName: "mapvx-map-assets",
plugins: [
new CacheableResponsePlugin({
statuses: [0, 200],
}),
new ExpirationPlugin({
maxEntries: 500,
maxAgeSeconds: 60 * 60 * 24, // 1 day
}),
],
})
)
// Images and icons → Cache-first
registerRoute(
({ request }) => request.destination === "image",
new CacheFirst({
cacheName: "mapvx-images",
plugins: [
new CacheableResponsePlugin({
statuses: [0, 200],
}),
new ExpirationPlugin({
maxEntries: 100,
maxAgeSeconds: 60 * 60 * 24 * 30, // 30 days
}),
],
})
)3. Manual Service Worker Registration
If you need manual control over Service Worker registration:
// Register Service Worker manually
async function registerServiceWorker() {
if ("serviceWorker" in navigator) {
try {
const registration = await navigator.serviceWorker.register("/mapvx-tiles-sw.js", {
scope: "/",
})
console.log("Service Worker registered successfully:", registration)
// Listen for updates
registration.addEventListener("updatefound", () => {
const newWorker = registration.installing
console.log("New Service Worker found")
newWorker?.addEventListener("statechange", () => {
if (newWorker.state === "installed") {
if (navigator.serviceWorker.controller) {
// New update available
console.log("New content available, please refresh")
} else {
// Content cached for offline use
console.log("Content cached for offline use")
}
}
})
})
} catch (error) {
console.error("Service Worker registration failed:", error)
}
}
}
// Call when your application starts
registerServiceWorker()Alternative Workbox Approaches
Using workbox-cli
For projects without complex build processes:
npm install workbox-cli --save-dev
npx workbox wizardThis creates a workbox-config.js file:
// workbox-config.js
export default {
globDirectory: "dist/",
globPatterns: ["**/*.{css,woff2,png,svg,jpg,js,pbf}"],
swDest: "dist/sw.js",
runtimeCaching: [
{
urlPattern: /\.pbf$/,
handler: "CacheFirst",
options: {
cacheName: "map-tiles",
expiration: {
maxEntries: 10000,
maxAgeSeconds: 60 * 60 * 24 * 7, // 1 week
},
},
},
],
}Using workbox-build
For more programmatic control:
// build-sw.js
import { generateSW } from "workbox-build"
generateSW({
globDirectory: "dist/",
globPatterns: ["**/*.{css,woff2,png,svg,jpg,js,pbf}"],
swDest: "dist/sw.js",
runtimeCaching: [
{
urlPattern: /api\.mapvx\.com/,
handler: "NetworkFirst",
options: {
cacheName: "api-cache",
networkTimeoutSeconds: 3,
},
},
],
})Using workbox-webpack-plugin
For Webpack projects:
// webpack.config.js
import { GenerateSW } from "workbox-webpack-plugin"
export default {
plugins: [
new GenerateSW({
swDest: "./dist/sw.js",
runtimeCaching: [
{
urlPattern: /\.pbf$/,
handler: "CacheFirst",
options: {
cacheName: "map-tiles",
},
},
],
}),
],
}Framework-Specific Integration
React/Next.js
// next.config.js
module.exports = {
async rewrites() {
return [
{
source: "/mapvx-tiles-sw.js",
destination: "/api/service-worker",
},
]
},
}
// pages/api/service-worker.js
import { readFileSync } from "fs"
import { join } from "path"
export default function handler(req, res) {
const swPath = join(process.cwd(), "public", "mapvx-tiles-sw.js")
const serviceWorker = readFileSync(swPath, "utf8")
res.setHeader("Content-Type", "application/javascript")
res.setHeader("Cache-Control", "no-cache")
res.send(serviceWorker)
}Angular
// angular.json - add to assets
"assets": [
"src/favicon.ico",
"src/assets",
"src/mapvx-tiles-sw.js"
]
// app.component.ts
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent implements OnInit {
ngOnInit() {
this.registerServiceWorker();
}
private async registerServiceWorker() {
if ('serviceWorker' in navigator) {
try {
await navigator.serviceWorker.register('/mapvx-tiles-sw.js');
console.log('Service Worker registered');
} catch (error) {
console.error('Service Worker registration failed:', error);
}
}
}
}Vue.js
// vue.config.js
module.exports = {
pwa: {
workboxPluginMode: "InjectManifest",
workboxOptions: {
swSrc: "src/mapvx-tiles-sw.js",
swDest: "mapvx-tiles-sw.js",
},
},
}
// main.js
import { createApp } from "vue"
import App from "./App.vue"
const app = createApp(App)
// Register Service Worker
if ("serviceWorker" in navigator) {
window.addEventListener("load", () => {
navigator.serviceWorker
.register("/mapvx-tiles-sw.js")
.then((registration) => {
console.log("SW registered: ", registration)
})
.catch((registrationError) => {
console.log("SW registration failed: ", registrationError)
})
})
}
app.mount("#app")Advanced Configuration
Custom Cache Strategies
// Custom cache strategy for specific map styles
registerRoute(
({ url }) => url.pathname.includes("/styles/custom"),
new CacheFirst({
cacheName: "custom-map-styles",
plugins: [
new CacheableResponsePlugin({ statuses: [0, 200] }),
new ExpirationPlugin({
maxEntries: 10,
maxAgeSeconds: 60 * 60 * 24 * 30, // 30 days
}),
],
})
)
// Skip caching for real-time data
registerRoute(
({ url }) => url.pathname.includes("/realtime") || url.pathname.includes("/live"),
new NetworkFirst({
cacheName: "realtime-data",
networkTimeoutSeconds: 3,
plugins: [
new CacheableResponsePlugin({ statuses: [0, 200] }),
new ExpirationPlugin({
maxEntries: 20,
maxAgeSeconds: 60, // 1 minute
}),
],
})
)Cache Management
// Clear old caches
self.addEventListener("activate", (event) => {
const cacheWhitelist = ["mapvx-tiles-pbf", "mapvx-map-assets", "mapvx-api-cache"]
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (!cacheWhitelist.includes(cacheName)) {
console.log("Deleting old cache:", cacheName)
return caches.delete(cacheName)
}
})
)
})
)
})Testing Service Worker
Chrome DevTools
- Open Chrome DevTools
- Go to Application tab
- Click Service Workers in the sidebar
- Verify registration and status
- Check Cache Storage for cached resources
Testing Script
// Test Service Worker functionality
async function testServiceWorker() {
if ("serviceWorker" in navigator) {
const registration = await navigator.serviceWorker.getRegistration()
if (registration) {
console.log("✅ Service Worker is registered")
console.log("Scope:", registration.scope)
console.log("State:", registration.active?.state)
// Test cache
const cache = await caches.open("mapvx-tiles-pbf")
const keys = await cache.keys()
console.log(`📦 Cached tiles: ${keys.length}`)
} else {
console.log("❌ Service Worker not registered")
}
} else {
console.log("❌ Service Worker not supported")
}
}
// Run test
testServiceWorker()Performance Monitoring
// Monitor cache performance
self.addEventListener("fetch", (event) => {
const start = performance.now()
event.respondWith(
// Your caching strategy here
caches.match(event.request).then((response) => {
const duration = performance.now() - start
if (response) {
console.log(`Cache hit: ${event.request.url} (${duration.toFixed(2)}ms)`)
} else {
console.log(`Cache miss: ${event.request.url} (${duration.toFixed(2)}ms)`)
}
return response || fetch(event.request)
})
)
})Important Notes
- HTTPS Required: Service Workers only work over HTTPS (except localhost)
- Same Origin: Service Worker must be served from the same origin
- Cache Limits: Be mindful of storage quotas (usually 50-100MB)
- Update Strategy: Plan for Service Worker updates and cache invalidation
- Fallback: Always provide network fallbacks for critical functionality
- Testing: Test thoroughly in different network conditions
Troubleshooting
Service Worker Not Registering
- Check browser console for errors
- Verify HTTPS connection
- Ensure file path is correct
- Check browser compatibility
Cache Not Working
- Verify cache names match
- Check network tab in DevTools
- Clear browser cache and re-test
- Verify cache strategies are correct
Performance Issues
- Monitor cache size limits
- Implement proper cache expiration
- Use appropriate cache strategies for different resource types
This implementation uses Workbox , Google’s library for adding offline support to web apps. Workbox provides flexible caching strategies and can be adapted to different frameworks and build processes. For more advanced configurations and integration options, refer to the official Workbox documentation .