w3c / resource-timing

Resource Timing
https://w3c.github.io/resource-timing/
Other
122 stars 37 forks source link

Resource Initiator Information Reporting #380

Open yashjoshi-dotcom opened 1 year ago

yashjoshi-dotcom commented 1 year ago

Abstract

The goal of this project is to extend the Resource Timing API to expose initiator information that will point to the resource that triggered the fetch of other resources. This will facilitate developers and RUM Providers to have interesting optimizations to make web apps work much faster.

Identifiers

Every resource gets a unique identifier known as ResourceID. This unique identifier attribute can be derived by combining the already present [URL] and the [startTime] attributes. The proposed algorithm for this will be essentially a string concatenation mechanism that will take the above-mentioned attributes as input and return a string that will be unique for each resource fetch. The ResourceID of the parent resource will be used to populate the Initiator field for every RT entry.

Approach

The feature's goal is to enable the creation of dependency trees from RUM (Real User Monitoring) data leading to a better understanding of the resource loading performance. In the basic case, it will be pointing to a particular culprit here which was responsible for loading of a particular resource:

  1. HTML Document is the initiator: For cases where the fetch is initiated by <script>, <img>, and <iframe> tags or by inline scripts, we point the initiator as the HTML document.
  2. CSS is the initiator: For cases where the fetch is initiated by @import, a background-image, or a font-face inside a stylesheet, we point to that stylesheet as the initiator for all such resources.
  3. Non-Inline Script is the initiator: Here the ancestor/direct-parent script is marked as the initiator. (Using TaskAttribution Infrastructure).

For case (3), things can get a little more complex, as there could be various blockers or dependencies that may be on the path for a resource to be loaded.

Examples

Example 1 - On Stack Script

Let's say we have some library code that a carousel relies upon to serve the resources. In such a scenario, if we want to optimize/speed up the loading of the requested resource, having a list of influencers/blockers pointing to both the carousel and library can be potentially more useful.

Example 2 - Preceding Deferred Script

Consider a case where we have the following:

<script defer src="library.js">
<script defer src="buttons.js">
<script defer src="carousel.js">

Here buttons.js will block the execution of the carousel, even if it won’t be part of the call stack for any resource loads.

Example 3 - On stack async script:

Consider another case where we have the following:

<script async src=”library.js”>
<script async src=”carousel.js”>

Carousel may rely on library to load its resources, but there aren’t any guarantees that it’d be there. So it may be in the stack, but not a blocker (in the sense that we may lose the race and end up with a JS error)

As presented in examples above , there are possibilities of having multiple scripts that influence a particular resource (script). These influencer scripts can be on stack or even off the stack in certain cases, where they block the execution of the current script.

Questions

So we’d like to ask the community:

  1. Single Initiator vs. Comprehensive Blocker List:

    • Is a single initiator useful on its own?
    • Or do we folks need the full list of potential blocker scripts in order to be able to use the initiator information?
  2. Handling "On Stack Async Script" Complexity:

    • Do we need to cover the “on stack async script” case? That runs a risk of adding a lot of complexity, only to support an inherently fragile coding pattern.

    Would appreciate your feedback and insights on this matter :)

nicjansma commented 1 year ago

Really excited about this!

Some initial thoughts on:

(1) single vs. multiple initiators

A single initiator will be much more valuable than what we have today (nothing), and if it can be the "nearest" or "most immediate" initiator, that will give RUM vendors a huge amount of insight into this dependency tree over what we have today.

If multiple initiators were offered, I'm sure RUM vendors would capture it, but we would have to better understand how to weigh each initiator in importance (so something like ordering of the entries would matter, or additional attributes on the entries list would be needed to understand why each initiator was there).

If we think we'll eventually go down the path of multiple initiators, we may want to start out with an array of initiators, even if we only ever fill it with a single entry to begin with.

(2) on-stack async scripts

I personally don't feel like the complexity of reporting on that is necessary.

(3) wrapping functions

Finally, one challenge existing RUM vendors, analytics scripts and/or security products may have is that JS functions can often be "wrapped" by those libraries (or other 3P resources) and replaced by vendor-supplied functions that do measuring/monitoring/intervention and re-call the original function.

Do you know how the initiator would be reported in this case? I'd be concerned e.g. RUM scripts would find themselves blamed for a lot of the downloads (which is what we see in dev tools, Lighthouse, etc commonly). I'm happy to provide some repro pages to test if you need.

If a single initiator was reporting just the "wrapper library" and the "original" caller was then 2nd in a list of initiators, reporting on multiple initiators instead could be beneficial here. How would a deep stack of e.g. a fetch() going through multiple scripts get reported?

yoavweiss commented 1 year ago

RE (3), I believe TaskAttribution would work fine, given its (mostly) caller semantics.

^^ @andreubotella @littledan @domenic - FYI RE the caller/registrar semantics discussion