- Keita Suzuki (@quasi-mod)
- Yoshisato Yanagisawa (@yoshisatoyanagisawa)
- Introduce timing info for the ServiceWorker Static Routing API in Resource Timing API and Navigation Timing API.
To bring better user experience with reducing frustration on rendering the web sites, it is good to use resource timing API to evaluate their page performance. As explained in the ServiceWorker static routing API's explainer, one of the motivations to introduce the API to the site is page performance improvement.
By utilizing the resource timing feature for the static routing API, developers gain the ability to gauge the latency introduced by the API itself. This includes measuring aspects like router evaluation time and cache lookup duration. Additionally, developers can verify if the initially matched source was ultimately used to fetch the resource, and if not, identify the alternative source that was employed.
The startup of ServiceWorkers, a web platform feature that brings application-like experience to users, is known to be a slow process. If the ServiceWorker intercepts loading the page resources, web users may need to wait for the startup to complete in order for the page loading to start.
Service Worker Static Routing API was introduced to mitigate this issue by letting the developers selectively choose whether the ServiceWorker should intercept the navigation, and allow them to specify when to not run ServiceWorker. In addition, it allows the developers to offload simple ServiceWorker operations like cache look up. i.e. they can return resources from CacheStorage without running ServiceWorkers.
Service Worker provides timing information to mark certain points in time. This is exposed and used by the navigation timing API as well as the resource timing API. It currently records two times:
- Start time
- Fetch event dispatch time
However, it currently does not have any fields related to the ServiceWorker Static Routing API. Developers would benefit from having fields that provide information such as:
- the matched route (the route that the Static Routing API evaluated)
- the actual source from which the resource was retrieved
- the time it took to match the route
This information will allow developers to measure the latency incurred by the API such as router evaluation time or time required to conduct cache lookup, or determine if the matched source is the final source used (can find out if the matched source failed to get the resource or not, and which source was used as the alternative).
We add the following two timing information:
- [Currently Under discussion] workerRouterEvaluationStart
- A
DOMHighResTimeStamp
, initially 0 - Time to start matching a request with registered router rules
- This field is currently under discussion, and will not be included in the initial spec change
- We will revisit this field once the static routing is adopted widely to determine with more data
- A
- workerCacheLookupStart
- A
DOMHighResTimeStamp
, initially 0 - Time to start looking up the cache storage when acessing from Cache API
- Recorded whenever the response is coming from Cache API
- Includes access when "cache" rule is specified in static routing API, or from SW fetch-event.
- A
In addition to the timestamp information, we also add the following two route source information:
- workerMatchedRouterSource
- A
RouterSource
, initially empty string - The enum string of the matched source (the source of result of router evaluation)
- This shall match to "network", "cache", "fetch-event", or "race-network-and-fetch-handler". If no rule is matched, it shall be an empty string.
- A
- workerFinalRouterSource
- A
RouterSource
, initially empty string - The enum string of the used source
- This shall match to "network", "cache", or "fetch-event"
- When a matched router source exists, this should match to the MatchedRouterSource, unless in "race-network-and-fetch-handler", where the winner of the race will be the final source (either "network" or "fetch-event"). Otherwise, it should remain as an empty string.
- A
// Add route inside ServiceWorker
addEventListener('install', (event) => {
event.addRoutes({
condition: {
urlPattern: {pathname: "/form/*"}
},
source: "fetch-event"
});
})
// Measure routerEvaluationTime
let timing = window.performance.timing;
let routerEvaluationTime = 0.0;
switch (timing.finalRouteSource) {
case "network":
// Indicates that the fetch fallback to network.
routerEvaluationTime = timing.fetchStart - timing.workerRouterEvaluationStart;
break;
case 'fetch-event':
routerEvaluationTime = timing.workerStart - timing.workerRouterEvaluationStart;
break;
case "cache":
// UNREACHABLE
break;
}
// Add route inside ServiceWorker
addEventListener('install', (event) => {
event.addRoutes({
condition: {
urlPattern: {pathname: "/form/*"}
},
source: "network"
});
})
// Measure routerEvaluationTime
let timing = window.performance.timing;
let routerEvaluationTime = 0.0;
switch (timing.finalRouteSource) {
case "network":
// Routed to network
routerEvaluationTime = timing.fetchStart - timing.workerRouterEvaluationStart;
break;
case "cache":
case "fetch-event":
// UNREACHABLE
break;
}
// Add route inside ServiceWorker
addEventListener('install', (event) => {
event.addRoutes({
condition: {
urlPattern: {pathname: "/form/*"}
},
source: "race-network-and-fetch-event"
});
})
// Measure routerEvaluationTime
let timing = window.performance.timing;
let routerEvaluationTime = 0.0;
switch (timing.finalRouteSource) {
case "network":
// Indicates that the network has won the race,
// or the fetch event has failed.
routerEvaluationTime = timing.fetchStart - timing.workerRouterEvaluationStart;
break;
case 'fetch-event':
// Indicates that the fetch has won the race.
routerEvaluationTime = timing.workerStart - timing.workerRouterEvaluationStart;
break;
case "cache":
// UNREACHABLE
break;
}
// Add route inside ServiceWorker
addEventListener('install', (event) => {
event.addRoutes({
condition: {
urlPattern: {pathname: "/form/*"}
},
source: "cache"
});
})
// Measure routerEvaluationTime and cacheLookupTime
let timing = window.performance.timing;
let routerEvaluationTime = 0.0;
let cacheLookupTime = 0.0;
switch (timing.FinalRouteSource) {
case "network":
// Cache miss. Fallback to network.
routerEvaluationTime = timing.cacheLookupStart - timing.routerEvaluationStart;
cacheLookupTime = time.fetchStart - time.workerCacheLookupStart;
break;
case "cache":
// Cache Hit.
routerEvaluationTime =
timing.cacheLookupStart - timing.workerRouterEvaluationStart;
cacheLookupTime =
time.responseStart - time.cacheLookupStart;
case "fetch-event":
// UNREACHABLE
break;
}
As mentioned above, the recorded fields will be different depending on the matched source. The fields to be recorded per source is as follows (✔ indicates recorded, ✘ indicates not recorded).
RouterEvaluationStart
|
CacheLookupStart
|
fetchStart
|
||
Matched Source | Fetch | ✔ | ✘ | ✔ |
Network | ✔ | ✘ | ✔ | |
Race (Network vs Fetch) | ✔ | ✘ | ✔ | |
Cache | ✔ | ✔ | ✘ | |
None Matched | ✔ | ✘ | ✔ |
In some situations, the actual source type will be different from the matched source type. This includes cases such as "race-network-and-fetch-event" where the result of the race will be the actual route, or "cache" where a cache miss occurs.
The full list of correspondence of the matched source type and the actual source type is as follows:
Actual Source | ||||
Fetch | Network | Cache | ||
Matched Source | Fetch | Fetch (Success) | Network (Fallback: Fetch handler is invalid) |
N/A |
Network | N/A | Network (Success) | N/A | |
Race (Network vs Fetch) | Fetch
(Fetch win) |
Network (Network win or Fetch fallback) |
N/A | |
Cache | N/A | Network (Fallback: Cache Missed) |
Cache (Cache hit) |
|
None matched | N/A (null) | N/A (null) | N/A |
The proposed features extend the widely used Resource Timing API, inheriting its security considerations.
As covered in the resource timing specification, the main security concern is the use of high resolution timers. The timing field of the resource timing is defined to use high resolution timers. However, if malicious actors gain access to high resolution timers, they could use the exposed fields as a part of Spectre attack to inspect any memory accesses. In addition, this could expose timing information about how the other origins loaded as well, leading to inferring information about the user's activity across origins.
To mitigate these risks, the same-origin policy is enforced by default, with specific attributes set to zero, as defined in the HTTP Fetch standard. Resource providers can selectively expose timing data by utilizing the Timing-Allow-Origin HTTP response header, granting access to specified domains.
Similar to the security concerns, privacy concern also inherits the same issue as the Resource Timing API. It has a potential of fingerprinting. While statistical fingerprinting poses a privacy risk, where malicious websites can infer a user's browsing history by analyzing resource loading times, the Resource Timing API doesn't significantly exacerbate this issue. Existing mechanisms, like the load event, already allow for limited cache timing measurements. Furthermore, cross-origin restrictions in HTTP Fetch, which is enforced by the Timing-Allow-Origin header, prevent this API from exposing additional information that could be exploited for fingerprinting.
When no matching rule is found, workerMatchedRouterSource
is an empty string, and we use
the ServiceWorker to fetch the resources. To indicate this case, there is a
discussion on what the workerFinalRouterSource
field should contain. We came up with
two possible solutions:
- Set empty string to
workerFinalRouterSource
as well - Set the actual source to
workerFinalRouteSource
("fetch-event" or "network", if it falls back)
We are currently planning to pursue Solution 1.
Solution 2 does expose more information to developers, but the exposed information is independent from ServiceWorker Static Routing API as it is about whether ServiceWorker fetch has succeeded or not. Although this should be taken into consideration in the future, we concluded that such information would be out of scope of Timing Info for the API.
When the cache is specified as the source and the resource is found in the cache
(cache hit), no fetch operation is performed.
To align the behavior with other fields, we will set fetchStart
to responseStart
when the resource is from cache.
When the final router source is from cache, the deliveryType
property should also
be set. Currently, deliveryType
has cache
as one of the values. However, this
points to the HTTP caching, and not
Cache API (CacheStorage) result. To avoid confusion,
we should introduce cache-storage
value to deliveryType
, where the value is set when
the response comes from Cache API.
The deliveryType cache-storage
is set either when the response is from the cache rule (with cache hit) in
static routing API, or when the response is coming from fetch handler response,
coming from the CacheStorage.