- 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 |
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 result. To avoid confusion,
we should introduce SW-cache
value to deliveryType
, where the value is set when
the response comes from Cache API (both for ServiceWorker and static routing API).