In a browser, PerformanceResourceTiming entries for third-party scripts and images report 0 for connectStart, requestStart, responseStart and transferSize, while same-origin entries are fully populated. Why, and what makes the real numbers visible?
answer
- another origin's numbers are not yours to read
- only the coarse start and end survive
- a response header from the other side opts in
- not the same switch as CORS body access
- transferSize zero also breaks cache detection
basics
~20 sDetailed Resource Timing attributes are cross-origin restricted: for a cross-origin resource the browser zeroes the connection, request and size fields unless the response carries a Timing-Allow-Origin header naming your origin (or *). Only startTime, responseEnd, duration and the resource name remain trustworthy without it.
solid answer
~50 sTiming detail leaks information about another origin — how long its server took, whether the response was cached, how big it was — so the browser hides it by default. For a cross-origin resource, `PerformanceResourceTiming` exposes only the coarse fields the requesting page already knows: `name`, `startTime`, `responseEnd`, `duration` and `initiatorType`. The redirect, DNS, connect, TLS, request and response sub-timings all read 0, `transferSize`, `encodedBodySize` and `decodedBodySize` read 0, and `nextHopProtocol` is an empty string. The unlock is a response header from the *other* origin: `Timing-Allow-Origin: *` or `Timing-Allow-Origin: https://your.site`. Nothing you do on your own page changes it, which is the practical point in an interview — for third-party scripts and image CDNs you must ask the vendor to send the header, and until they do, the only honest thing your RUM can say about that resource is its total wall-clock duration.
code
javascript · 12 linesfor (const entry of performance.getEntriesByType('resource')) {
const opaque = entry.responseStart === 0 && entry.duration > 0;
console.log({
url: entry.name,
kind: entry.initiatorType,
total: Math.round(entry.duration),
serverWait: opaque ? null : Math.round(entry.responseStart - entry.requestStart),
bytes: opaque ? null : entry.transferSize,
timingOpaque: opaque
});
}go deeper
Know that resource entries exist for every asset the page fetches, and that a cross-origin one shows far less detail than a same-origin one unless the other server allows it.
Explain exactly which fields are gated — redirect, DNS, connect, TLS, request, response and the three size fields — that Timing-Allow-Origin is the unlock, and that it is a separate switch from CORS.
Show that you reason about the blind spot: what your monitoring can still assert for an opaque vendor asset, how the missing transferSize corrupts cache-hit reporting, and treating the header as a vendor requirement.
Own it as a procurement and platform question — timing transparency as a contractual requirement for third parties on the critical path, and a default Timing-Allow-Origin policy on every origin the organisation controls.
## The API and what it normally gives you Every subresource the page fetches produces a `PerformanceResourceTiming` entry with `entryType: 'resource'`. You read them with `performance.getEntriesByType('resource')` or observe them live. A fully-populated entry is a network waterfall in object form, with monotonically ordered timestamps: `startTime` → `redirectStart`/`redirectEnd` → `fetchStart` → `domainLookupStart`/`domainLookupEnd` → `connectStart` → `secureConnectionStart` → `connectEnd` → `requestStart` → `responseStart` → `responseEnd` plus three size fields — `transferSize` (bytes over the wire including headers), `encodedBodySize` (compressed body) and `decodedBodySize` (after decompression) — and `nextHopProtocol` (`h2`, `h3`, `http/1.1`). From those you derive the useful deltas yourself: DNS is `domainLookupEnd - domainLookupStart`, TLS is `connectEnd - secureConnectionStart`, server think-time is `responseStart - requestStart`, download is `responseEnd - responseStart`. ## Why cross-origin entries are censored All of that is information about a server your page does not control. Exposing it to arbitrary embedding pages is a side channel: response size distinguishes between resources on a private origin, and timing differences leak whether a resource was in the user's cache — which, for a URL that only some users can fetch, leaks state about the user. So the platform's default for a cross-origin resource is to reveal only what the embedding page inevitably knows anyway: that it made a request at `startTime` and the request finished at `responseEnd`. Everything in between is zeroed rather than omitted. The fields still exist, they just read 0 — which is why the bug presents as "my DNS time is suspiciously always zero for the CDN" rather than as an exception. ## The unlock: Timing-Allow-Origin The other origin opts in by sending a response header: ``` Timing-Allow-Origin: * Timing-Allow-Origin: https://shop.example.com ``` If the header matches your origin (or is `*`), the browser fills in the full entry. It is a distinct mechanism from CORS: `Access-Control-Allow-Origin` governs whether your script may *read the response body*, while `Timing-Allow-Origin` governs whether it may read the *timing metadata*. A resource can be readable and timing-opaque, or timing-transparent and body-opaque — they are independent switches. Many CDNs send `Timing-Allow-Origin: *` by default or behind a configuration toggle; many analytics and ad vendors do not. This is also why the fix is a conversation, not a code change. On your own origin you set the header and move on. For a third party, your leverage is the contract, and "send Timing-Allow-Origin" is a reasonable line item when you are negotiating with a vendor whose script sits on your critical path. ## What you can still do without it `duration` (equivalently `responseEnd - startTime`) is always available, and so is `initiatorType` (`script`, `img`, `css`, `fetch`, `xmlhttprequest`, `link`…). That is enough to answer "how long did the third-party tag block us for and what kind of resource was it", which is often the question that matters. What you cannot do is attribute the slowness — you cannot say whether the vendor's server was slow, whether the connection setup was slow, or whether the payload was simply large. ## The cache-hit heuristic and why TAO matters for it A widely used trick reads a cache hit off the size fields: if `transferSize` is 0 while `decodedBodySize` is greater than 0, the response came from cache rather than the network. It is a genuinely useful signal for validating a caching strategy. But `transferSize` is one of the zeroed fields, so the heuristic reports "everything is cached" for every timing-opaque cross-origin resource — a false positive that will happily corrupt a caching dashboard if nobody notices. ## The related LCP trap The same header has a second effect that catches people out. A `largest-contentful-paint` entry for a cross-origin image reports `renderTime` as 0 unless the image response carries `Timing-Allow-Origin`; collection code then has to fall back to `loadTime`, which is slightly earlier. If your hero image is served from an image CDN without the header, your reported LCP is systematically a little optimistic. Setting `Timing-Allow-Origin` on the image host fixes it. ## Checking it quickly Iterate the resource entries and flag the opaque ones — an entry whose `responseStart` is 0 but whose `duration` is non-zero is timing-opaque, not instantaneous. Counting those tells you how much of your waterfall your monitoring is actually blind to, and it is usually more than the team assumes.
- How does Timing-Allow-Origin differ from Access-Control-Allow-Origin?They gate different things. `Access-Control-Allow-Origin` decides whether your script may read the response *body* under CORS. `Timing-Allow-Origin` decides whether your page may read the response's *timing and size metadata* in Resource Timing. Neither implies the other: a CORS-enabled API can still be timing-opaque, and an image with full timing exposure can still have an unreadable body. Both are set by the responding origin, not the requester.
- Your dashboard says nearly every third-party asset is served from cache. What would you suspect?The cache-hit heuristic `transferSize === 0 && decodedBodySize > 0` misfiring. Cross-origin resources without `Timing-Allow-Origin` report both size fields as 0, so the first half of the condition is trivially true while the second half is false — or, in code that only checks `transferSize`, every opaque resource is counted as a hit. Restrict the heuristic to entries whose timing is actually exposed.
- Does a redirect on a cross-origin resource change what you can see?Yes, and it hides more than people expect. `redirectStart` and `redirectEnd` are among the timing-gated fields, so a cross-origin resource behind a redirect chain reports zero redirect time while its `duration` silently includes the extra round trips. You see a slow asset with no explanation. Even with `Timing-Allow-Origin`, every response in the chain must send the header for the redirect timings to be exposed.
saying these in an interview costs you the question
- Thinks zeroed timing fields mean the request was instant
- Believes Access-Control-Allow-Origin also unlocks timing data
- Tries to fix it with a change on the requesting page
- Counts transferSize zero as a cache hit for any resource
- Assumes duration is unavailable for cross-origin resources