What happens
ResourceCache will store any host's script or stylesheet that passes the HTTP policy check, so third-party tag-manager, A/B-testing and personalization scripts get cached alongside the site's own assets. Those scripts are session-scoped by nature: a stored copy carries the campaign, bucket and variation decisions of the render that fetched it. Replaying it into a later render makes the snapshot show content that does not correspond to what the live page renders — usually less of it, occasionally more.
This is a correctness problem for the served HTML rather than a performance one, and it is silent: the render still comes back 200, non-empty and indexable, so nothing in the worker's stats or the job result looks wrong. The only symptom is content missing from the cached page.
Measured
On a live storefront's mobile home page, with the cache enabled, the served HTML lost ~5,290 characters of text, 22 links and 16 images on every render.
The comparison was interleaved off/on/off/on, 4 pairs, so that any site-side session variance lands in both arms. Both arms were internally identical (range 0 across renders on text length, link count and image count) and differed only by arm. Render duration was unchanged between arms, so this is not a settle-timing race where the faster arm snapshots earlier — a personalization module simply never rendered.
Everything the cache had stored while rendering that page was third-party: a tag manager, its rule bundles, a personalization vendor's api_dynamic.js, an on-site marketing vendor's creative bundles, and the usual ad/analytics pixels. Not one first-party asset — #109 is why that was all it had, and the two compound: that issue excludes everything safe to cache, this one keeps everything that isn't.
Mechanism
isCacheableRequest (packages/browser/src/ResourceCache.ts, 1.16.0) gates on method, navigation, resource type and auth/cookie headers only. There is no host restriction:
isCacheableRequest(req) {
if (req.method() !== 'GET') return false;
if (req.isNavigationRequest()) return false;
if (!CACHEABLE_RESOURCE_TYPES.has(req.resourceType())) return false;
const headers = req.headers();
if (headers['authorization'] || headers['cookie']) return false;
return true;
}
Anything with a positive max-age is then eligible, and tag vendors serve their bundles with long max-ages (observed: 1200s to 30 days).
Suggested fix
Restrict caching to the hosts a site serves its own assets from. The library can't infer those — a storefront's asset host is frequently a different registrable domain from the navigation host — so it needs to be configurable: e.g. resourceCache.hosts: string[], matched as host-or-subdomain suffixes, with an empty list preserving today's cache-any-host behavior. A same-site default derived from the navigation URL handles the simple case but misses separate asset domains, so it can't be the only mechanism.
Worth deciding whether the restrictive behavior should be the default. The cache's entire value is the site's own bundles (megabytes per render); third-party tags contribute nothing to the snapshot while carrying this risk. A deployment that already blocks tag hosts through block.urlPatterns never sees the problem, which is a good way for it to stay unnoticed.
How to test for it
Interleave the arms (off/on/off/on) rather than running all-off then all-on: these pages vary on their own between renders, and an A,A,B,B ordering attributes site-side session variance to the cache. Then treat a cross-arm difference as real only when it exceeds the within-arm spread. Comparing text length, anchor count, image count and JSON-LD count of the served HTML is enough to see this one; a raw byte diff is too noisy to read.
What happens
ResourceCachewill store any host's script or stylesheet that passes the HTTP policy check, so third-party tag-manager, A/B-testing and personalization scripts get cached alongside the site's own assets. Those scripts are session-scoped by nature: a stored copy carries the campaign, bucket and variation decisions of the render that fetched it. Replaying it into a later render makes the snapshot show content that does not correspond to what the live page renders — usually less of it, occasionally more.This is a correctness problem for the served HTML rather than a performance one, and it is silent: the render still comes back 200, non-empty and indexable, so nothing in the worker's stats or the job result looks wrong. The only symptom is content missing from the cached page.
Measured
On a live storefront's mobile home page, with the cache enabled, the served HTML lost ~5,290 characters of text, 22 links and 16 images on every render.
The comparison was interleaved off/on/off/on, 4 pairs, so that any site-side session variance lands in both arms. Both arms were internally identical (range 0 across renders on text length, link count and image count) and differed only by arm. Render duration was unchanged between arms, so this is not a settle-timing race where the faster arm snapshots earlier — a personalization module simply never rendered.
Everything the cache had stored while rendering that page was third-party: a tag manager, its rule bundles, a personalization vendor's
api_dynamic.js, an on-site marketing vendor's creative bundles, and the usual ad/analytics pixels. Not one first-party asset — #109 is why that was all it had, and the two compound: that issue excludes everything safe to cache, this one keeps everything that isn't.Mechanism
isCacheableRequest(packages/browser/src/ResourceCache.ts, 1.16.0) gates on method, navigation, resource type and auth/cookie headers only. There is no host restriction:Anything with a positive
max-ageis then eligible, and tag vendors serve their bundles with long max-ages (observed: 1200s to 30 days).Suggested fix
Restrict caching to the hosts a site serves its own assets from. The library can't infer those — a storefront's asset host is frequently a different registrable domain from the navigation host — so it needs to be configurable: e.g.
resourceCache.hosts: string[], matched as host-or-subdomain suffixes, with an empty list preserving today's cache-any-host behavior. A same-site default derived from the navigation URL handles the simple case but misses separate asset domains, so it can't be the only mechanism.Worth deciding whether the restrictive behavior should be the default. The cache's entire value is the site's own bundles (megabytes per render); third-party tags contribute nothing to the snapshot while carrying this risk. A deployment that already blocks tag hosts through
block.urlPatternsnever sees the problem, which is a good way for it to stay unnoticed.How to test for it
Interleave the arms (off/on/off/on) rather than running all-off then all-on: these pages vary on their own between renders, and an A,A,B,B ordering attributes site-side session variance to the cache. Then treat a cross-arm difference as real only when it exceeds the within-arm spread. Comparing text length, anchor count, image count and JSON-LD count of the served HTML is enough to see this one; a raw byte diff is too noisy to read.