The strategies so far - Cache First, Network First, Stale While Revalidate - trade freshness for availability in different ways, but all of them intercept the request and answer from the cache when possible. It is natural to pick one favorite strategy and apply it to the whole app. But every app has resources where the right move is the opposite: deliberately never cache, or deliberately never touch the network. These are the two extreme strategies - Network Only and Cache Only.
Network Only means the request goes straight to the network, the service worker does not substitute a cached copy, and a network error surfaces honestly as an error. This is how you should treat mutations, sensitive endpoints, realtime checks and telemetry. A stale cached answer here is a lie: answering 'is this slot still free' or a payment charge with old data corrupts the domain instead of speeding it up.
Take a field notes app that an inspector runs over a flaky connection. Sending a new note is a POST, and you must never answer a POST from cache. No network - the product handles the error: it queues the command rather than serving an old response and pretending it went through. Background Sync is a retry queue for delivering the command, not a caching strategy. The retry must be idempotent and carry the same operation ID, or a twice-delivered command creates two notes instead of one.
Cache Only is the mirror extreme: the answer comes only from Cache Storage, and the worker never goes to the network. It is justified for a guaranteed precached resource or a pre-downloaded offline pack - the reference material and area maps the inspector loaded before heading out. The key condition: a cache miss must be impossible by contract or have a fallback. Serve Cache Only for something you did not store in advance, and offline you get a hard failure with no backup path.
The cost of Cache Only is that you own invalidation entirely. Nothing will refresh that pack on its own: it lives exactly as it was placed. That is cheap and instant, but a stale offline pack is your responsibility, and its version is tracked by hand alongside the app's version.
One app almost always mixes several strategies, and how you split them is not a technical detail but part of the product contract. Before moving everything into Workbox routes, it helps to write down for each route: owner, freshness budget, offline value, max bytes, invalidation method, auth sensitivity and fallback.
| Class | Strategy | Constraint |
|---|---|---|
| Hashed JS/CSS/fonts | Precache / Cache First | Revision + cleanup |
| HTML navigation | Network First | Timeout + offline fallback |
| Images | Cache First or SWR | maxEntries + maxAge |
| Public GET API | Network First / SWR | schema version + TTL |
| Private GET API | IndexedDB/domain cache or network | user namespace + logout purge |
| Mutation | Network Only + outbox | idempotency + conflict policy |
Hashed JS, CSS and fonts are identified by their content hash on their own, so they go into precache with cleanup of old revisions. HTML navigation is driven by Network First with a timeout and an offline fallback. Images use Cache First or SWR with maxEntries and maxAge limits. A public GET API uses Network First or SWR with a schema version and TTL. A private GET uses a domain cache in IndexedDB under the user's namespace, purged on logout. Mutations use Network Only plus an outbox with idempotency and a conflict policy.
The telling failure is applying one strategy to everything at once. Cache First on HTML navigation freezes people on a stale build; Network First on hashed assets burns the network on files that are immutable by definition; caching a private GET without a logout purge shows the next user someone else's data on a shared device. The matrix exists so that none of these failures happens by oversight.