Skip to content

yoshisatoyanagisawa/service-worker-static-routing-api

 
 

Repository files navigation

Explainer: ServiceWorker Static Routing API

Authors

Participate

Background

ServiceWorker is a web platform feature that brings application-like experience to users. It works in the background of a web browser to provide a cache feature, offline support, and other functionality.

A problem and a solution

Starting ServiceWorkers is known to be a slow process, and web users need to wait for its startup if the ServiceWorker intercepts loading the page resources. At the same time, the ServiceWorker brings flexibility to the transport layer, and it behaves as a client-side proxy. Developers can implement offline support or provide client-side content modification with it. Since using the ServiceWorker has a certain overhead, we need to use it wisely.

Currently, ServiceWorkers intercept all navigation for pages within their scope, which can cause a performance penalty. To avoid this penalty, we need a way to specify when ServiceWorkers and JavaScript should not be run.

NavigationPreload

NavigationPreload was introduced in Chrome 59 (June, 2017). To avoid the navigation being blocked by the ServiceWorker startup, the browser sends a request to a network while starting a ServiceWorker to hide a performance penalty to start a ServiceWorker.

However, there is still a delay while the ServiceWorker startup. This may happen on slow devices, which can be unacceptable for partners who care about web performance. Additionally, this feature changes the execution order, which can make it difficult to analyze and debug issues. As a result, some partners have stopped using navigation preload.

Unlike navigation preload, the static routing API proposed below allows developers to exclude the ServiceWorker from the navigation critical path where it is configured. We can avoid the delay for the slow ServiceWorker start-up in that case. The execution order is easy to understand and predict.

A proposal

We propose a new version of declarative routing API originally proposed by Jake Archibald in 2018-2019 (github, site). It defines set of static routing rules for the following purpose:

  • be able to bypass a ServiceWorker for particular requests.
  • speed up simple offline-first, online-first routes by avoiding ServiceWorker startup time.
  • be polyfillable.
  • be extensible.

Our proposal is heavily based on the proposal, with minor changes to use the modern primitives (URLPattern), and simplify the API surface. This is a full picture of the proposal. However, since it is too large, we will start from something minimal (Refer to the FAQ section for the detailed list of changes applied in this proposal).

From the user's perspective, this proposal brings performance improvement by skipping waiting for ServiceWorker where it is not needed. Even with slow devices, users do not need to wait for the page load blocked by slow ServiceWorker start-up. Not directly covered by this explainer, but we have offline first support in our big picture, and more pages can be used offline when it is realized.

WebIDL

Below is the WebIDL of the API. You may find it more complicated than what it can do now. That is because it is designed to allow evolution to the full picture in the future.

// This follows Jake's proposal.  Router is implemented in the InstallEvent.
interface InstallEvent : ExtendableEvent {
  // `addRoutes()` is used to define static routes.
  // Not matching all rules means fallback to the regular process.
  // i.e. fetch handler.
  // Promise is rejected for invalid `rules` e.g. syntax error.
  Promise<undefined> addRoutes((RouterRule or sequence<RouterRule>) rules);
}

// RouterRule defines the condition and source of the static routes.  One entry contains one rule.
dictionary RouterRule {
  // In the first version, only the single condition and source should be enough because we only
  // support urlPattern, and "network".
  RouterCondition condition;
  RouterSourceEnum source;
}

// Defines when to respond from the source.
dictionary RouterCondition {
  // https://wicg.github.io/urlpattern/#typedefdef-urlpatterninput
  // For a USVString input, a ServiceWorker script's URL is used as a base URL.
  URLPatternInput urlPattern;
};

enum RouterSourceEnum { "network" };

Note that the rules are evalauted sequentially with added order. It means that if addRoutes() is called multiple times, rules added ealier will be evaluated ealier.

Examples

Bypassing ServiceWorker for particular resources

// Go straight to the network and bypass invoking "fetch" handlers for URLs that start with '/form/'.
addEventListener('install', (event) => {
  event.addRoutes({
    condition: {
      urlPattern: new URLPattern({pathname: "/form/*"})
    },
    source: "network"
  });
})

// Go straight to the network and bypass invoking "fetch" handlers for URLs that start
// with '/videos/' and '/images/'.
addEventListener('install', (event) => {
  event.addRoutes([{
    condition: {
      urlPattern: new URLPattern({pathname: "/images/*"})
    },
    source: "network"
  },
  {
    condition: {
      urlPattern: new URLPattern({pathname: "/videos/*"})
    },
    source: "network"
  }]);
});

Origin Trial

We (Google Chrome team) will start the origin trial, which allows developers to opt-in the feature on their sites. Regarding the introduction and basic registration process, please refer to this page.

Once a token string is generated from the dashboard, developers need to set the HTTP response header to their ServiceWorker files.

Origin-Trial: TOKEN_GOES_HERE

One important thing to mention here is that the header has to be set to the ServiceWorker script, not to the page. Also, HTML meta tags are not supported. The feature will take effect after the ServiceWorker registration.

For local testing, you can enable the feature by flipping the Service Worker Static Router flag from chrome://flags.

FAQ

Is a demo available for the API?

Please visit https://sw-static-routing-demo.glitch.me/ with the latest Chrome.

How is the proposal different from Jake’s original proposal?

We propose addRoutes() to set routes with specified routes instead of add() and get(). Unlike add() or get(), addRoutes() can take a list of rules in addition to a single rule. It can be called multiple times in case if needed (e.g. imported third-party service worker script already added some routes). Web developers need to use the browser mechanisms like devtools to check the latest router rules. addRoutes() is a part of the install event 1. Since addRoutes() is only the method to set the router rules, we put it as a part of the install event. When the install listener is executed, no routes are set. Web developers can call addRoutes() to set routes at that time.

Our proposal uses URLPattern, which was not available when Jake made the original proposal. It is natural evolution to use URLPattern instead of URL related conditions in the proposal.

Summary

  • Introduce addRoutes() method, and won’t provide add() or get() methods.
    • addRoutes() method sets ServiceWorker routes with specified routes. To allow third party services to use the API, the method can be called multiple times.
  • URL related conditions are merged into URLPattern.

How does it work if there is no fetch handler?

For the "network" source, we can safely ignore the routes if there is no fetch handler, except for a debugging purpose. In the full picture, we introduce "cache", "fetch-event", and "race-network-and-fetch-handler" sources. The "cache" source should look up a request from the cache storage even if there is no fetch handler. Moreover, it does not need to run the fetch handlers regardless of cache hit or cache miss. On the other hand, the "fetch-event" and "race-network-and-fetch-handler" sources run the fetch handlers. addRoutes() should return a promise rejected with a TypeError if these sources are used while no fetch handlers are set.

How does it work with empty fetch listeners?

If all the fetch listeners are empty functions, routes that only have the "network" source can be ignored, except for a debugging purpose. Like the no fetch handler case above, the "cache" source should look up the cache storage regardless of the empty fetch handler or not. However, unlike the no fetch handler case above, addRoutes() should return a promise resolved with undefined for the "fetch-event" and "race-network-and-fetch-handler" sources because the fetch handler exists. During the navigation, the fetch handler may not need to run for these sources because they are empty, which is allowed by w3c/ServiceWorker#1674.

How does it work with Navigation Preload?

Routes are evaluated before Navigation Preload. If no routes are matched, navigation falls back to the regular ServiceWorker fetch handler path, and Navigation Preload would be used if it is configured.

How would the full picture be rolled out?

The full picture is large, and it is not easy to implement it at once. We plan to gradually roll it out with the following order:

  • RouterURLPatternCondition, RouterNetworkSource (this proposal)
  • RouterRequestCondition, RouterAndCondition, RouterNotCondition
  • RouterRunningStatusCondition, RouterOrCondition
  • RouterTimeCondition
  • RouterCacheSource, RouterFetchSource, RouterSourceBehaviorEnum, allowing sequence of sources. (offline/online-first support)
  • Stale-While-Revalidate support.
  • race-network-and-fetch-event support

How Chrome implements this?

The Google Chrome team starts the Origin Trial from M116, and the implementation has slightly been changed from M117. It has been explained in a separate document.

There was registerRouter(). For ease of understanding the latest routes, it can be called once. However, it made it difficult for the third-party services to add routes. To solve the situation, the method has been renamed to addRoutes() and can be called multiple times.

Footnotes

  1. We followed Jake's proposal to use the install event.

About

A repository for the ServiceWorker static routing API.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published