Why transferSize: 0 Does Not Always Mean a Cache Hit

A script from your CDN domain shows transferSize 0, so the Resource Timing dashboard marks it as cached. How do you tell whether it really came from cache?

Why transferSize: 0 Does Not Always Mean a Cache Hit
TLDR
  • A zero transfer size can mean local cache reuse or restricted cross-origin information.
  • Firefox 158 adds support for the cache value of deliveryType.
  • Cache use does not necessarily mean that no network request occurred.

A CDN resource can report zero without being a cache hit

Counting every transferSize === 0 entry as cached can misclassify cross-origin downloads. MDN documents two relevant causes: local cache reuse and restrictions on cross-origin size information.

For the size-based check, look for zero transferSize together with a positive decodedBodySize. The latter represents the uncompressed response body. When both values are zero, the entry fails that check; keep it unclassified instead of counting a confirmed hit.

A zero beside a script on a separate CDN domain is therefore insufficient evidence that loading it used no bandwidth. Missing information and an actual zero cost require different interpretations.

Record the delivery signal as well as the sizes

Firefox 158 supports the cache value of PerformanceResourceTiming.deliveryType. The property describes how a resource was delivered, but browser support varies. Preserve the distinction between an unsupported property and a supported property with an empty value.

This example logs the inputs to classification rather than forcing every resource into a cache-or-network bucket:

const observer = new PerformanceObserver((list) => {
  console.table(list.getEntries().map((entry) => ({
    url: entry.name,
    deliveryType: "deliveryType" in entry
      ? entry.deliveryType : "unsupported",
    transferSize: entry.transferSize,
    decodedBodySize: entry.decodedBodySize
  })));
});

observer.observe({ type: "resource", buffered: true });

The buffered option includes entries already present in the buffer; the observer also receives new resource entries. Here, unsupported is an application label, not an API-defined delivery type.

Do not automatically relabel an empty string as a network download. An entry may lack an applicable delivery classification or have information withheld. Keeping the raw values lets you revisit the classification without inventing information.

Timing-Allow-Origin belongs on the resource response

The server delivering a cross-origin resource can use Timing-Allow-Origin to expose timing information to a named origin. For a page at https://app.example, an example response header is:

Timing-Allow-Origin: https://app.example

Inspect the CDN file’s response, not just your HTML page’s headers. Adding an observer on the page cannot remove restrictions imposed on that resource.

The Resource Timing specification also permits additional cross-origin restrictions even when the header is present. Treat it as part of the exposure configuration, not a guarantee that every size field will be available.

A cache result can still involve a network round trip

HTTP cache reuse can follow validation with the server. Reusing the stored body after validation saves a body download, but a request and response still occur. Cache usage and the absence of network activity are different metrics.

The deliveryType cache classification covers local and validated reuse. It does not identify memory versus disk storage, so a fast duration is not enough to label an entry as a memory-cache hit.

Service workers add another boundary: the page’s timing describes its interaction with the worker. The worker’s own timeline is needed to inspect its internal fetches.

Keep confirmed cache use separate from unknown entries, then investigate validation and service-worker paths. A dashboard should preserve those distinctions before turning zeros into a claimed performance improvement.