Deploying a Service Worker‑Based Offline Cache in Safari: Minimal Design and Operational Checklist
Safari’s service‑worker support is solid but quirky. This guide shows the exact requirements, a lean design that works across Safari 15+, how to respect data boundaries, what to check in devtools, and how to handle common failure modes.
24 Aug 2025, 17:02 UTC

Requirements
To run a Service Worker (SW) in Safari you need:
- HTTPS on the same origin – the SW script and all assets must be served over TLS.
- Same‑origin
manifest.json– Safari treats the app as installable only if the manifest exists on the root of the origin. - Explicit
scope– Safari will only activate the SW for URLs that match thescopeattribute exactly. For a single‑page app, setscopeto "./" so it covers the entire site. - Safari 15+ – earlier versions lack full Cache API support.
Example manifest.json snippet:
{
"name": "My PWA",
"short_name": "PWA",
"start_url": "/index.html",
"display": "standalone",
"icons": [
{"src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png"},
{"src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png"}
]
}
Minimal Design
The simplest, most reliable strategy for Safari is a single SW that splits requests into two families:
- Static assets – Cache‑First (e.g.,
.css,.js, images). - Dynamic API calls – Network‑First with a fallback to a cached response if the network fails.
Use a runtime cache with limits to avoid hitting Safari’s ~50 MB per‑origin quota. The following script demonstrates this approach.
// service-worker.js
const STATIC_CACHE = 'static-v1';
const RUNTIME_CACHE = 'runtime-v1';
const STATIC_ASSETS = [
'/index.html',
'/styles/main.css',
'/scripts/app.js',
'/icons/icon-192.png',
'/icons/icon-512.png'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(STATIC_CACHE).then(cache => cache.addAll(STATIC_ASSETS))
);
});
self.addEventListener('activate', event => {
// Clean up old caches if needed
event.waitUntil(
caches.keys().then(keys => {
return Promise.all(keys.filter(k => k !== STATIC_CACHE && k !== RUNTIME_CACHE)
.map(k => caches.delete(k))
);
})
);
});
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
// API calls – Network First
if (url.pathname.startsWith('/api/')) {
event.respondWith(
fetch(event.request).catch(() => caches.match(event.request))
);
return;
}
// Static assets – Cache First
event.respondWith(
caches.match(event.request).then(cached => {
if (cached) return cached;
return caches.open(RUNTIME_CACHE).then(cache => {
return fetch(event.request).then(response => {
// Store a copy for future use
if (response.ok) cache.put(event.request, response.clone());
return response;
});
});
})
);
});
Key points:
- All fetch events are intercepted; the SW never touches the DOM.
- Runtime cache entries are capped by a manual cleanup routine (not shown) or by using
Cache.putwith a size limit. - API calls are not cached aggressively, preserving fresh data while still offering offline resilience.
Trust & Data Boundaries
The SW runs in a dedicated thread and has no direct access to the page’s DOM or JavaScript context. Data flows only through the fetch event and the Cache API. Therefore:
- Validate every response before caching: check
response.okand, if needed, inspect headers or body for security tokens. - Never store sensitive data unencrypted in the cache; if you must, encrypt it on the page before it reaches the SW.
- Because the SW can be installed by any origin that serves it, keep the manifest and SW registration on the same origin to avoid cross‑origin hijacking.
Operational Checks
After deployment, verify the following using Safari’s Web Inspector:
- Service Worker status: Open the
Applicationpanel, selectService Workers, and ensure the script is listed as Active and Registered. - Cache contents: In the same panel, view
Cache Storageand confirm thatstatic-v1contains all assets listed inSTATIC_ASSETS. - Fetch interception: Reload the page, open the
Networktab, and observe that requests to static assets show afrom cachestatus. API calls should showfrom networkunless the network is offline. - Console logs: Add
console.logstatements to theinstallandactivateevents. Errors will surface in Safari’s console if the SW fails to parse or register.
Failure Modes & Mitigation
Safari’s implementation has a few quirks that can break a seemingly correct SW:
- Activation delay: Historically, Safari would not fire the
activateevent until the page was reloaded. If you rely onactivateto clean old caches, trigger a reload after SW installation or use a manual cleanup on subsequent visits. - Quota limits: Exceeding the ~50 MB limit silently evicts the oldest cache entries. Keep
STATIC_ASSETSlean and periodically purge the runtime cache using a size‑based eviction policy. - Unsupported APIs: Safari does not support
BackgroundSyncorPeriodicBackgroundSync. Avoid these APIs; a simplefetchretry loop in the page is a safer fallback. - Cache corruption: If the cache store becomes corrupted, subsequent
caches.matchcalls may returnundefined. Handle this by falling back to a network request and, if that fails, serving a minimal offline page from the cache.
Example fallback for a missing static asset:
event.respondWith(
caches.match(event.request).then(cached => {
if (cached) return cached;
// Asset missing – serve a generic offline page
return caches.match('/offline.html');
})
);
Practical Verification Checklist
- Serve the site on
https://example.comwith a valid TLS cert. - Deploy
service-worker.jsandmanifest.jsonto the root. - Open Safari 15+, navigate to the site, and confirm the SW is active.
- Clear the
Cache Storagevia devtools, reload, and verify the page still loads via network. - Toggle network offline in Safari’s devtools, reload, and confirm the app serves cached static assets and returns a cached API response if available.
- Check console for any
installoractivateerrors.
By following this minimal design and validation steps, you can deliver a robust offline experience in Safari without relying on experimental APIs or complex cache strategies.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.