Some assets in your web application may be infrequently used, very large, or vary based on the user's device (such as responsive images) or language. These are instances where precaching may be an anti-pattern, and you should rely on runtime caching instead.
In Workbox, you can handle runtime caching for assets using the workbox-routing
module to match routes, and handle caching strategies for them with the workbox-strategies
module.
Caching strategies
You can handle most routes for assets with one of the built in caching strategies. They're covered in detail earlier in this documentation, but here are a few worth recapping:
- Stale While Revalidate uses a cached response for a request if it's available and updates the cache in the background with a response from the network. Therefore, if the asset isn't cached, it will wait for the network response and use that. It's a fairly safe strategy, as it regularly updates cache entries that rely on it. The downside is that it always requests an asset from the network in the background.
- Network First tries to get a response from the network first. If a response is received, it passes that response to the browser and saves it to a cache. If the network request fails, the last cached response will be used, enabling offline access to the asset.
- Cache First checks the cache for a response first and uses it if available. If the request isn't in the cache, the network is used and any valid response is added to the cache before being passed to the browser.
- Network Only forces the response to come from the network.
- Cache Only forces the response to come from the cache.
You can apply these strategies to select requests using methods offered by workbox-routing
.
Applying caching strategies with route matching
workbox-routing
exposes a registerRoute
method to match routes and handle them with a caching strategy. registerRoute
accepts a Route
object that in turn accepts two arguments:
- A string, regular expression, or a match callback to specify route matching criteria.
- A handler for the route—typically a strategy provided by
workbox-strategies
.
Match callbacks are preferred to match routes, as they provide a context object that includes the Request
object, the request URL string, the fetch event, and a boolean of whether the request is a same-origin request.
The handler then handles the matched route. In the following example, a new route is created that matches same-origin image requests coming, applying the cache first, falling back to network strategy.
// sw.js
import { registerRoute, Route } from 'workbox-routing';
import { CacheFirst } from 'workbox-strategies';
// A new route that matches same-origin image requests and handles
// them with the cache-first, falling back to network strategy:
const imageRoute = new Route(({ request, sameOrigin }) => {
return sameOrigin && request.destination === 'image'
}, new CacheFirst());
// Register the new route
registerRoute(imageRoute);
Using multiple caches
Workbox allows you to bucket cached responses into separate Cache
instances using the cacheName
option available in the bundled strategies.
In the following example, images use a stale-while-revalidate strategy, whereas CSS and JavaScript assets use a cache-first falling back to network strategy. The route for each asset places responses into separate caches, by adding the cacheName
property.
// sw.js
import { registerRoute, Route } from 'workbox-routing';
import { CacheFirst, StaleWhileRevalidate } from 'workbox-strategies';
// Handle images:
const imageRoute = new Route(({ request }) => {
return request.destination === 'image'
}, new StaleWhileRevalidate({
cacheName: 'images'
}));
// Handle scripts:
const scriptsRoute = new Route(({ request }) => {
return request.destination === 'script';
}, new CacheFirst({
cacheName: 'scripts'
}));
// Handle styles:
const stylesRoute = new Route(({ request }) => {
return request.destination === 'style';
}, new CacheFirst({
cacheName: 'styles'
}));
// Register routes
registerRoute(imageRoute);
registerRoute(scriptsRoute);
registerRoute(stylesRoute);
Setting an expiry for cache entries
Be aware of storage quotas when managing service worker cache(s). ExpirationPlugin
simplifies cache maintenance and is exposed by workbox-expiration
. To use it, specify it in the configuration for a caching strategy:
// sw.js
import { registerRoute, Route } from 'workbox-routing';
import { CacheFirst } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
// Evict image cache entries older thirty days:
const imageRoute = new Route(({ request }) => {
return request.destination === 'image';
}, new CacheFirst({
cacheName: 'images',
plugins: [
new ExpirationPlugin({
maxAgeSeconds: 60 * 60 * 24 * 30,
})
]
}));
// Evict the least-used script cache entries when
// the cache has more than 50 entries:
const scriptsRoute = new Route(({ request }) => {
return request.destination === 'script';
}, new CacheFirst({
cacheName: 'scripts',
plugins: [
new ExpirationPlugin({
maxEntries: 50,
})
]
}));
// Register routes
registerRoute(imageRoute);
registerRoute(scriptsRoute);
Complying with storage quotas can be complicated. It's good practice to consider users who may be experiencing storage pressure, or want to make the most efficient use of their storage. Workbox's ExpirationPlugin
pairs can help in achieving that goal.
Cross-origin considerations
The interaction between your service worker and cross-origin assets is considerably different than with same-origin assets. Cross-Origin Resource Sharing (CORS) is complicated, and that complexity extends to how you handle cross-origin resources in a service worker.
Opaque responses
When making a cross-origin request in no-cors
mode, the response can be stored in a service worker cache and even be used directly by the browser. However, the response body itself can't be read via JavaScript. This is known as an opaque response.
Opaque responses are a security measure intended to prevent the inspection of a cross-origin asset. You can still make requests for cross-origin assets and even cache them, you just can't read the response body or even read its status code!
Remember to opt into CORS mode
Even if you load cross-origin assets that do set permissive CORS headers that allow you read responses, the body of cross-origin response may still be opaque. For example, the following HTML will trigger no-cors
requests that will lead to opaque responses regardless of what CORS headers are set:
<link rel="stylesheet" href="https://example.com/path/to/style.css">
<img src="https://example.com/path/to/image.png">
To explicitly trigger a cors
request that will yield a non-opaque response, you need to explicitly opt-in to CORS mode by adding the crossorigin
attribute to your HTML:
<link crossorigin="anonymous" rel="stylesheet" href="https://example.com/path/to/style.css">
<img crossorigin="anonymous" src="https://example.com/path/to/image.png">
This is important to remember when routes in your service worker cache subresources loaded at runtime.
Workbox may not cache opaque responses
By default, Workbox takes a cautious approach to caching opaque responses. As it's impossible to examine the response code for opaque responses, caching an error response can result in a persistently broken experience if a cache-first or cache-only strategy is used.
If you need to cache an opaque response in Workbox, you should use a network-first or stale-while-validate strategy to handle it. Yes, this means that the asset will still be requested from the network every time, but it ensures that failed responses won't persist, and will eventually be replaced by usable responses.
If you use another caching strategy and an opaque response is returned, Workbox will warn you that the response wasn't cached when in development mode.
Force caching of opaque responses
If you are absolutely certain that you want to cache an opaque response using a cache-first or cache only strategy, you can force Workbox to do so with the workbox-cacheable-response
module:
import {Route, registerRoute} from 'workbox-routing';
import {NetworkFirst, StaleWhileRevalidate} from 'workbox-strategies';
import {CacheableResponsePlugin} from 'workbox-cacheable-response';
const cdnRoute = new Route(({url}) => {
return url === 'https://cdn.google.com/example-script.min.js';
}, new CacheFirst({
plugins: [
new CacheableResponsePlugin({
statuses: [0, 200]
})
]
}))
registerRoute(cdnRoute);
Opaque Responses and the navigator.storage
API
To avoid leakage of cross-domain information, there's significant padding added to the size of an opaque response used for calculating storage quota limits. This affects how the navigator.storage
API reports storage quotas.
This padding varies by browser, but for Chrome, the minimum size that any single cached opaque response contributes to the overall storage used is approximately 7 megabytes. You should keep this in mind when determining how many opaque responses you want to cache, since you could easily exceed storage quotas much sooner than you'd otherwise expect.